Compare commits

..

55 Commits

Author SHA1 Message Date
11530dd7fc Dialogue uses arrays 2026-05-03 01:25:25 +03:00
3fd167ebff More events added 2026-05-03 01:11:14 +03:00
5952a96ee6 Fixed fonts sizes 2026-05-03 00:02:24 +03:00
76c3ead4a8 game_start event works 2026-05-02 23:43:48 +03:00
39a053d4ee Game mode API 2026-05-02 20:25:16 +03:00
c5da977857 Dualogue event API doc/samples 2026-05-02 18:25:30 +03:00
3e7b0169d5 Lua behavior tree 2026-05-01 13:54:44 +03:00
f918c5cefb Better handling of lua tasks 2026-05-01 04:02:47 +03:00
976ced3731 Lua-based behavior tree node 2026-05-01 00:31:06 +03:00
0fd8deaf53 direct save/load action database 2026-04-30 20:21:18 +03:00
4d843c18c7 Lua API 2026-04-30 19:07:35 +03:00
0ed83966da Lua action APIs 2026-04-30 10:03:56 +03:00
998984f75a Root motion fixed now 2026-04-29 18:45:37 +03:00
02fa78764a Lua API implemented 2026-04-29 14:13:50 +03:00
abe6eef6b3 Modules update 2026-04-29 12:53:13 +03:00
cca732b41b Fixes for event system 2026-04-27 20:53:04 +03:00
8507a3a501 Event system 2026-04-27 18:45:01 +03:00
b9cce0248a Labels and actuators work perfectly! 2026-04-27 09:03:47 +03:00
fa49bb5005 Actuators 2026-04-27 06:55:05 +03:00
37441aa8fd Motion fixed 2026-04-27 06:04:14 +03:00
a1b74aa2d5 Now Path Following component works 2026-04-27 05:37:41 +03:00
c80d9c96e6 AI motion refactoring 2026-04-27 05:24:45 +03:00
a75db85027 Can disable physics stepping 2026-04-26 22:15:15 +03:00
7563937ab8 Prefab placement at cursor 2026-04-26 20:51:20 +03:00
425bb8411d Fixed crash with entity destruction 2026-04-26 17:56:19 +03:00
9b29b68b33 Prefab editing and window hiding 2026-04-26 17:45:57 +03:00
7557c710fb Added prefabs 2026-04-26 16:43:37 +03:00
ce2f6c1306 Navmesh generation works with cell grids 2026-04-26 14:28:54 +03:00
e0e8e316d4 Fixed navmesh 2026-04-26 00:50:55 +03:00
abd2dc22d3 Now can test smart object action on player character again 2026-04-26 00:00:36 +03:00
a5df60769f Now repeated smart object action works perfectly 2026-04-25 23:08:00 +03:00
75ba39895f Teleport node works 2026-04-25 21:55:21 +03:00
2cff982473 Delays and animation conflicts fixed 2026-04-25 20:59:50 +03:00
3bd2801d1d Smart objects work! 2026-04-25 09:04:12 +03:00
2e358275f0 Path following works great 2026-04-25 01:17:31 +03:00
5ed7552164 Path following 2026-04-25 00:11:21 +03:00
2b3482da88 Normal display tool implemented 2026-04-24 21:12:02 +03:00
1d2c330481 Material fixes, playing with navmesh 2026-04-24 20:29:54 +03:00
a0d2561587 navmesh 2026-04-24 04:37:38 +03:00
e95b904f4e Underwater effect 2026-04-23 01:55:09 +03:00
9d4fad1d10 Water plane 2026-04-23 01:29:53 +03:00
4335a8cb05 Skybox and sun 2026-04-23 00:38:20 +03:00
d55bf970e0 Swim animations 2026-04-22 21:38:33 +03:00
30814ea35a Added animations for swimming 2026-04-22 19:50:14 +03:00
35f50f7f51 Fixed buoyancy 2026-04-22 18:57:33 +03:00
ca5b5b3052 Buoyancy 2026-04-22 17:27:40 +03:00
7e4e8f6638 Color atlas serialization fixes 2026-04-21 11:42:46 +03:00
c6fb3bb463 Camera works now 2026-04-21 03:59:45 +03:00
1411990def Forward/backwards fixed 2026-04-21 03:27:53 +03:00
1488d7d918 Camera change... 2026-04-21 03:04:47 +03:00
ef708fa14a Fixed game mode menu 2026-04-20 22:10:17 +03:00
6d7fcb1157 Not so well working game mode 2026-04-20 20:21:27 +03:00
4313d190f9 Characters are fully functional now 2026-04-20 12:23:31 +03:00
a2173114b9 Some questionable changes 2026-04-19 23:50:00 +03:00
fb6881998c Added character physics but it does not work yet 2026-04-19 23:45:00 +03:00
455 changed files with 215156 additions and 1925 deletions

View File

@@ -164,9 +164,9 @@ function(blender_import_vrm BLEND VRM EDITABLE RIG)
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py -- ${VRM_NAME}.vrm ${BLEND} ${EDITABLE} ${RIG}
COMMAND ${CMAKE_COMMAND} -D FILE=${BLEND} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${BLEND}
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
${VRM}
${VRM} ${CMAKE_BINARY_DIR}/assets/blender/mixamo
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
endfunction()

View File

@@ -73,8 +73,6 @@ FileSystem=resources/fonts
[LuaScripts]
FileSystem=lua-scripts
#[Characters]
#FileSystem=./characters
[Audio]
FileSystem=./audio/gui

View File

@@ -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
}
}
}

View File

@@ -8,13 +8,21 @@ find_package(SDL2 REQUIRED)
find_package(Jolt REQUIRED)
find_package(OgreProcedural REQUIRED CONFIG)
# Build RecastNavigation from copied source (RTTI-compatible)
add_subdirectory(recastnavigation)
# Collect all source files
set(EDITSCENE_SOURCES
main.cpp
EditorApp.cpp
GameMode.cpp
systems/EditorUISystem.cpp
systems/SceneSerializer.cpp
systems/PhysicsSystem.cpp
systems/BuoyancySystem.cpp
systems/EditorSunSystem.cpp
systems/EditorSkyboxSystem.cpp
systems/EditorWaterPlaneSystem.cpp
systems/LightSystem.cpp
systems/CameraSystem.cpp
systems/LodSystem.cpp
@@ -23,10 +31,47 @@ set(EDITSCENE_SOURCES
systems/ProceduralMaterialSystem.cpp
systems/ProceduralMeshSystem.cpp
systems/CellGridSystem.cpp
systems/NormalDebugSystem.cpp
systems/RoomLayoutSystem.cpp
systems/FurnitureLibrary.cpp
systems/StartupMenuSystem.cpp
systems/PlayerControllerSystem.cpp
systems/CharacterSlotSystem.cpp
systems/AnimationTreeSystem.cpp
systems/BehaviorTreeSystem.cpp
systems/NavMeshSystem.cpp
recast/TileCacheNavMesh.cpp
recast/PartitionedMesh.cpp
recast/fastlz.c
systems/CharacterSystem.cpp
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
@@ -43,6 +88,8 @@ set(EDITSCENE_SOURCES
ui/TriangleBufferEditor.cpp
ui/CharacterSlotsEditor.cpp
ui/AnimationTreeEditor.cpp
ui/AnimationTreeTemplateEditor.cpp
ui/CharacterEditor.cpp
ui/CellGridEditor.cpp
ui/LotEditor.cpp
ui/DistrictEditor.cpp
@@ -53,7 +100,25 @@ set(EDITSCENE_SOURCES
ui/ClearAreaEditor.cpp
ui/FurnitureTemplateEditor.cpp
ui/StartupMenuEditor.cpp
ui/PlayerControllerEditor.cpp
ui/BuoyancyInfoEditor.cpp
ui/GoapBlackboardEditor.cpp
ui/BehaviorTreeEditor.cpp
ui/InlineBehaviorTreeEditor.cpp
ui/NavMeshEditor.cpp
ui/ActionDatabaseEditor.cpp
ui/ActionDatabaseSingletonEditor.cpp
ui/ActionDebugEditor.cpp
ui/ComponentRegistration.cpp
components/GoapBlackboard.cpp
components/GoapExpression.cpp
components/GoapGoal.cpp
components/ActionDatabase.cpp
components/ActionDatabaseModule.cpp
components/ActionDebugModule.cpp
components/GoapBlackboardModule.cpp
components/NavMeshModule.cpp
components/LightModule.cpp
components/CameraModule.cpp
components/LodModule.cpp
@@ -64,13 +129,33 @@ set(EDITSCENE_SOURCES
components/TriangleBufferModule.cpp
components/CharacterSlotsModule.cpp
components/AnimationTreeModule.cpp
components/AnimationTreeTemplateModule.cpp
components/AnimationTree.cpp
components/CharacterModule.cpp
components/CellGridModule.cpp
components/CellGridEditorsModule.cpp
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
components/SunModule.cpp
components/SkyboxModule.cpp
camera/EditorCamera.cpp
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
lua/LuaGameModeApi.cpp
)
set(EDITSCENE_HEADERS
@@ -81,6 +166,11 @@ set(EDITSCENE_HEADERS
components/Relationship.hpp
components/PhysicsCollider.hpp
components/RigidBody.hpp
components/BuoyancyInfo.hpp
components/WaterPhysics.hpp
components/WaterPlane.hpp
components/Sun.hpp
components/Skybox.hpp
components/Light.hpp
components/Camera.hpp
components/Lod.hpp
@@ -93,19 +183,66 @@ set(EDITSCENE_HEADERS
components/TriangleBuffer.hpp
components/CharacterSlots.hpp
components/AnimationTree.hpp
components/Character.hpp
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
systems/CellGridSystem.hpp
systems/NormalDebugSystem.hpp
systems/RoomLayoutSystem.hpp
systems/FurnitureLibrary.hpp
systems/ProceduralMaterialSystem.hpp
systems/ProceduralMeshSystem.hpp
systems/CharacterSlotSystem.hpp
systems/AnimationTreeSystem.hpp
systems/BehaviorTreeSystem.hpp
systems/NavMeshSystem.hpp
recast/TileCacheNavMesh.hpp
recast/PartitionedMesh.hpp
recast/fastlz.h
systems/CharacterSystem.hpp
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
systems/PhysicsSystem.hpp
systems/BuoyancySystem.hpp
systems/EditorSunSystem.hpp
systems/EditorSkyboxSystem.hpp
systems/EditorWaterPlaneSystem.hpp
systems/LightSystem.hpp
systems/CameraSystem.hpp
systems/LodSystem.hpp
@@ -128,6 +265,8 @@ set(EDITSCENE_HEADERS
ui/TriangleBufferEditor.hpp
ui/CharacterSlotsEditor.hpp
ui/AnimationTreeEditor.hpp
ui/AnimationTreeTemplateEditor.hpp
ui/CharacterEditor.hpp
ui/CellGridEditor.hpp
ui/LotEditor.hpp
ui/DistrictEditor.hpp
@@ -138,9 +277,37 @@ set(EDITSCENE_HEADERS
ui/ClearAreaEditor.hpp
ui/FurnitureTemplateEditor.hpp
ui/StartupMenuEditor.hpp
ui/PlayerControllerEditor.hpp
ui/BuoyancyInfoEditor.hpp
ui/GoapBlackboardEditor.hpp
ui/GoapBlackboardComponentEditor.hpp
ui/BehaviorTreeEditor.hpp
ui/InlineBehaviorTreeEditor.hpp
ui/NavMeshEditor.hpp
ui/NavMeshGeometrySourceEditor.hpp
ui/ActionDatabaseEditor.hpp
ui/ActionDatabaseSingletonEditor.hpp
ui/ActionDebugEditor.hpp
components/GoapBlackboard.hpp
components/GoapExpression.hpp
components/NavMesh.hpp
components/BehaviorTree.hpp
components/GoapAction.hpp
components/GoapGoal.hpp
components/ActionDatabase.hpp
components/ActionDebug.hpp
camera/EditorCamera.hpp
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
lua/LuaGameModeApi.hpp
)
add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS})
@@ -157,10 +324,154 @@ target_link_libraries(editSceneEditor
nlohmann_json::nlohmann_json
Jolt::Jolt
OgreProcedural::OgreProcedural
RecastNavigation::Recast
RecastNavigation::Detour
RecastNavigation::DetourTileCache
RecastNavigation::DetourCrowd
RecastNavigation::DebugUtils
lua
)
target_include_directories(editSceneEditor PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/Recast/Include
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/Detour/Include
${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/LuaGameModeApi.cpp
lua/LuaEntityApi.cpp
GameMode.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)
# Test: EventParams C++ API (standalone, no Lua dependency)
add_executable(event_params_test
tests/event_params_test.cpp
)
target_link_libraries(event_params_test
lua
)
target_include_directories(event_params_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: Game Mode Lua API
add_executable(game_mode_lua_test
tests/game_mode_lua_test.cpp
tests/lua_test_stubs.cpp
)
target_link_libraries(game_mode_lua_test
lua
)
target_include_directories(game_mode_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Copy local resources (materials, etc.)
@@ -183,5 +494,9 @@ add_custom_command(TARGET editSceneEditor POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_SOURCE_DIR}/resources.cfg"
"${CMAKE_CURRENT_BINARY_DIR}/resources.cfg"
# Re-copy editScene-specific resources so they aren't overwritten
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/resources"
"${CMAKE_CURRENT_BINARY_DIR}/resources"
COMMENT "Copying resources to editSceneEditor build directory"
)

View File

@@ -1,7 +1,12 @@
#include <iostream>
#include "EditorApp.hpp"
#include "GameMode.hpp"
#include "systems/EditorUISystem.hpp"
#include "systems/PhysicsSystem.hpp"
#include "systems/BuoyancySystem.hpp"
#include "systems/EditorSunSystem.hpp"
#include "systems/EditorSkyboxSystem.hpp"
#include "systems/EditorWaterPlaneSystem.hpp"
#include "systems/LightSystem.hpp"
#include "systems/CameraSystem.hpp"
#include "systems/LodSystem.hpp"
@@ -11,8 +16,21 @@
#include "systems/ProceduralMeshSystem.hpp"
#include "systems/CharacterSlotSystem.hpp"
#include "systems/AnimationTreeSystem.hpp"
#include "systems/BehaviorTreeSystem.hpp"
#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"
#include "components/EntityName.hpp"
#include "components/Transform.hpp"
@@ -21,6 +39,12 @@
#include "components/PhysicsCollider.hpp"
#include "components/RigidBody.hpp"
#include "components/GeneratedPhysicsTag.hpp"
#include "components/BuoyancyInfo.hpp"
#include "components/InWater.hpp"
#include "components/WaterPhysics.hpp"
#include "components/WaterPlane.hpp"
#include "components/Sun.hpp"
#include "components/Skybox.hpp"
#include "components/Light.hpp"
#include "components/Camera.hpp"
#include "components/Lod.hpp"
@@ -33,10 +57,41 @@
#include "components/TriangleBuffer.hpp"
#include "components/CharacterSlots.hpp"
#include "components/AnimationTree.hpp"
#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"
#include "components/ActionDatabase.hpp"
#include "components/ActionDebug.hpp"
#include "components/BehaviorTree.hpp"
#include "components/GoapBlackboard.hpp"
#include "components/PrefabInstance.hpp"
#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/EventBus.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"
#include "lua/LuaGameModeApi.hpp"
//=============================================================================
// ImGuiRenderListener Implementation
@@ -44,10 +99,12 @@
ImGuiRenderListener::ImGuiRenderListener(Ogre::ImGuiOverlay *imguiOverlay,
EditorUISystem *uiSystem,
Ogre::RenderWindow *renderWindow)
Ogre::RenderWindow *renderWindow,
EditorApp *editorApp)
: m_imguiOverlay(imguiOverlay)
, m_uiSystem(uiSystem)
, m_renderWindow(renderWindow)
, m_editorApp(editorApp)
{
m_lastTime = m_timer.getMilliseconds();
}
@@ -69,6 +126,33 @@ void ImGuiRenderListener::preViewportUpdate(
if (m_uiSystem) {
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 &&
m_editorApp->getGamePlayState() == EditorApp::GamePlayState::Menu) {
StartupMenuSystem *sms = m_editorApp->getStartupMenuSystem();
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(
@@ -98,6 +182,8 @@ EditorApp::EditorApp()
, m_overlaySystem(nullptr)
, m_imguiOverlay(nullptr)
, m_currentModifiers(0)
, m_gameMode(GameMode::Editor)
, m_gamePlayState(GamePlayState::Menu)
{
}
@@ -112,19 +198,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_characterSlotSystem.reset();
// Destroy dialogue system before other systems
m_dialogueSystem.reset();
m_startupMenuSystem.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();
@@ -169,7 +263,6 @@ void EditorApp::setup()
m_imguiOverlay = initialiseImGui();
if (m_imguiOverlay) {
m_imguiOverlay->setZOrder(300);
m_imguiOverlay->show();
ImGui::StyleColorsDark();
}
@@ -187,6 +280,10 @@ void EditorApp::setup()
// Setup UI system
m_uiSystem = std::make_unique<EditorUISystem>(
m_world, m_sceneMgr, getRenderWindow());
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>(
@@ -194,6 +291,29 @@ void EditorApp::setup()
m_physicsSystem->initialize();
m_uiSystem->setPhysicsSystem(m_physicsSystem.get());
// Setup buoyancy system (requires physics system)
// Get the physics wrapper from the physics system
m_buoyancySystem = std::make_unique<BuoyancySystem>(
m_world, m_physicsSystem->getPhysicsWrapper());
m_buoyancySystem->initialize();
m_sunSystem =
std::make_unique<EditorSunSystem>(m_world, m_sceneMgr);
m_skyboxSystem = std::make_unique<EditorSkyboxSystem>(
m_world, m_sceneMgr);
m_waterPlaneSystem = std::make_unique<EditorWaterPlaneSystem>(
m_world, m_sceneMgr);
// Apply debug setting if it was set before system creation
if (m_debugBuoyancy) {
m_buoyancySystem->setDebugEnabled(true);
}
// Set buoyancy system in UI system for configuration
if (m_uiSystem) {
m_uiSystem->setBuoyancySystem(m_buoyancySystem.get());
}
// Setup light system
m_lightSystem = std::make_unique<EditorLightSystem>(m_world,
m_sceneMgr);
@@ -239,16 +359,129 @@ void EditorApp::setup()
m_world, m_sceneMgr);
m_animationTreeSystem->initialize();
// Setup Character physics system (needed by BehaviorTreeSystem)
m_characterSystem =
std::make_unique<CharacterSystem>(m_world, m_sceneMgr);
m_characterSystem->initialize();
m_behaviorTreeSystem = std::make_unique<BehaviorTreeSystem>(
m_world, m_sceneMgr, m_animationTreeSystem.get(),
m_characterSystem.get());
// Setup NavMesh system
m_navMeshSystem =
std::make_unique<NavMeshSystem>(m_world, m_sceneMgr);
// Setup SmartObject system (requires NavMesh, BehaviorTree, and AnimationTree)
m_smartObjectSystem = std::make_unique<SmartObjectSystem>(
m_world, m_sceneMgr, m_navMeshSystem.get(),
m_behaviorTreeSystem.get());
// Wire up AnimationTreeSystem for animation state machine control
m_smartObjectSystem->setAnimationTreeSystem(
m_animationTreeSystem.get());
// 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);
m_cellGridSystem->initialize();
// Wire CellGridSystem into NavMeshSystem so it can collect
// batched frame/furniture geometry from StaticGeometry.
m_navMeshSystem->setCellGridSystem(m_cellGridSystem.get());
// Setup NormalDebug system (disabled by default)
m_normalDebugSystem = std::make_unique<NormalDebugSystem>(
m_world, m_sceneMgr, m_cellGridSystem.get());
// Wire NormalDebugSystem into UI for toggle
if (m_uiSystem) {
m_uiSystem->setNormalDebugSystem(
m_normalDebugSystem.get());
}
// Setup RoomLayout system
m_roomLayoutSystem =
std::make_unique<RoomLayoutSystem>(m_world, m_sceneMgr);
m_roomLayoutSystem->initialize();
// 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);
if (m_gameMode == GameMode::Game) {
// Load startup menu scene configured in editor.
// This must happen before show() so the
// StartupMenuComponent entity exists for font preparation.
SceneSerializer serializer(m_world, m_sceneMgr);
Ogre::LogManager::getSingleton().logMessage(
"Game mode: Loading startup_menu.json...");
if (serializer.loadFromFile("startup_menu.json",
m_uiSystem.get())) {
PrefabSystem prefabSys(m_world, m_sceneMgr);
prefabSys.resolveInstances();
Ogre::LogManager::getSingleton().logMessage(
"Game mode: startup_menu.json loaded");
} else {
Ogre::LogManager::getSingleton().logMessage(
"Game mode: Failed to load startup_menu.json: " +
serializer.getLastError());
}
// Pre-load fonts before showing overlay so the font is
// added to the atlas before OGRE builds it in createFontTexture().
// The StartupMenuComponent entity is now available from the
// startup_menu.json scene loaded above.
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)
m_imguiOverlay->show();
// Add default entities to UI cache
for (auto &e : m_defaultEntities) {
m_uiSystem->addEntity(e);
@@ -256,13 +489,60 @@ void EditorApp::setup()
// Create and register ImGui render listener
m_imguiListener = std::make_unique<ImGuiRenderListener>(
m_imguiOverlay, m_uiSystem.get(), getRenderWindow());
m_imguiOverlay, m_uiSystem.get(), getRenderWindow(),
this);
getRenderWindow()->addListener(m_imguiListener.get());
// Register input listeners
addInputListener(this);
addInputListener(getImGuiInputListener());
// 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);
editScene::registerLuaGameModeApi(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);
}
if (m_gameMode == GameMode::Game) {
// Queue "game_start" event after Lua scripts are loaded
// so Lua subscribers registered via ecs.subscribe_event()
// will receive the event.
EventBus::getInstance().send("game_start");
}
// Game mode can be set externally before setup() is called
m_setupComplete = true;
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"Setup failed: " + Ogre::String(e.what()));
@@ -270,6 +550,111 @@ void EditorApp::setup()
}
}
void EditorApp::setGameMode(GameMode mode)
{
if (m_setupComplete) {
Ogre::LogManager::getSingleton().logMessage(
"setGameMode ignored: cannot change mode after setup");
return;
}
m_gameMode = mode;
editScene::setEditSceneGameMode(mode == GameMode::Game ?
editScene::GameMode::Game :
editScene::GameMode::Editor);
if (m_gameMode == GameMode::Game) {
m_gamePlayState = GamePlayState::Menu;
editScene::setEditSceneGamePlayState(
editScene::GamePlayState::Menu);
if (m_uiSystem)
m_uiSystem->setEditorUIEnabled(false);
} else {
m_gamePlayState = GamePlayState::Menu;
editScene::setEditSceneGamePlayState(
editScene::GamePlayState::Menu);
if (m_uiSystem)
m_uiSystem->setEditorUIEnabled(true);
}
}
void EditorApp::setDebugBuoyancy(bool enabled)
{
m_debugBuoyancy = enabled;
// Apply debug setting to buoyancy system if it exists
if (m_buoyancySystem) {
m_buoyancySystem->setDebugEnabled(enabled);
}
// Log the debug mode change only if OGRE is initialized
// (setDebugBuoyancy may be called before OGRE setup)
try {
if (Ogre::LogManager::getSingletonPtr()) {
Ogre::LogManager::getSingleton().logMessage(
"Buoyancy debug mode: " +
Ogre::StringConverter::toString(enabled));
}
} catch (...) {
// Ignore if OGRE not initialized yet
}
}
void EditorApp::setGamePlayState(GamePlayState state)
{
m_gamePlayState = state;
editScene::setEditSceneGamePlayState(
state == GamePlayState::Playing ?
editScene::GamePlayState::Playing :
state == GamePlayState::Paused ?
editScene::GamePlayState::Paused :
editScene::GamePlayState::Menu);
// Grab/ungrab mouse based on gameplay state
if (m_gameMode == GameMode::Game) {
if (state == GamePlayState::Playing) {
setWindowGrab(true);
} else if (state == GamePlayState::Menu) {
setWindowGrab(false);
}
}
}
void EditorApp::clearScene()
{
// Destroy all entities with EditorMarkerComponent
std::vector<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();
}
}
if (m_uiSystem) {
m_uiSystem->clearEntityCache();
}
}
void EditorApp::startNewGame(const Ogre::String &scenePath)
{
clearScene();
SceneSerializer serializer(m_world, m_sceneMgr);
if (serializer.loadFromFile(scenePath, m_uiSystem.get())) {
PrefabSystem prefabSys(m_world, m_sceneMgr);
prefabSys.resolveInstances();
setGamePlayState(GamePlayState::Playing);
Ogre::LogManager::getSingleton().logMessage(
"Game started: loaded scene " + scenePath);
// Send "scene_ready" event after scene is loaded and
// entities/components are populated and ready to run.
EventBus::getInstance().send("scene_ready");
} else {
Ogre::LogManager::getSingleton().logMessage(
"Failed to load scene: " + serializer.getLastError());
}
}
void EditorApp::setupECS()
{
// Register components
@@ -282,6 +667,9 @@ void EditorApp::setupECS()
m_world.component<PhysicsColliderComponent>();
m_world.component<GeneratedPhysicsTag>();
m_world.component<RigidBodyComponent>();
m_world.component<BuoyancyInfo>();
m_world.component<WaterPhysics>();
m_world.component<WaterPlane>();
// Register light and camera components
m_world.component<LightComponent>();
@@ -311,8 +699,59 @@ void EditorApp::setupECS()
// Register AnimationTree component
m_world.component<AnimationTreeComponent>();
// Register AnimationTreeTemplate component
m_world.component<AnimationTreeTemplate>();
// Register Character component
m_world.component<CharacterComponent>();
// Register game components
m_world.component<StartupMenuComponent>();
m_world.component<DialogueComponent>();
m_world.component<PlayerControllerComponent>();
m_world.component<InWater>();
// Register environment components
m_world.component<SunComponent>();
m_world.component<SkyboxComponent>();
// Register AI/GOAP components
// ActionDatabase is now a singleton, registered in ActionDatabaseModule
m_world.component<ActionDebug>();
m_world.component<BehaviorTreeComponent>();
m_world.component<GoapBlackboard>();
// 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>();
// Register CellGrid/Town components
CellGridModule::registerComponents(m_world);
// Register PrefabInstance component
m_world.component<PrefabInstanceComponent>();
// Register Item and Inventory components
m_world.component<ItemComponent>();
m_world.component<InventoryComponent>();
}
void EditorApp::createDefaultEntities()
@@ -373,10 +812,13 @@ void EditorApp::createGrid()
grid->end();
Ogre::SceneNode *gridNode =
m_gridNode =
m_sceneMgr->getRootSceneNode()->createChildSceneNode(
"GridNode");
gridNode->attachObject(grid);
m_gridNode->attachObject(grid);
// Set initial visibility based on game mode
m_gridNode->setVisible(m_gameMode == GameMode::Editor);
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"Grid creation failed: " + Ogre::String(e.what()));
@@ -417,12 +859,15 @@ void EditorApp::createAxes()
axisZ->end();
// Create axis node
Ogre::SceneNode *axisNode =
m_axisNode =
m_sceneMgr->getRootSceneNode()->createChildSceneNode(
"AxisNode");
axisNode->attachObject(axisX);
axisNode->attachObject(axisY);
axisNode->attachObject(axisZ);
m_axisNode->attachObject(axisX);
m_axisNode->attachObject(axisY);
m_axisNode->attachObject(axisZ);
// Set initial visibility based on game mode
m_axisNode->setVisible(m_gameMode == GameMode::Editor);
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"Axis creation failed: " + Ogre::String(e.what()));
@@ -431,77 +876,157 @@ void EditorApp::createAxes()
bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
{
// Update camera
if (m_camera) {
m_camera->update(evt.timeSinceLastFrame);
if (m_gameMode == GameMode::Editor) {
// Update camera
if (m_camera) {
m_camera->update(evt.timeSinceLastFrame);
}
} else if (m_gameMode == GameMode::Game) {
if (m_gamePlayState == GamePlayState::Playing) {
if (m_playerControllerSystem) {
m_playerControllerSystem->update(
evt.timeSinceLastFrame);
}
}
}
// Update physics
if (m_physicsSystem) {
m_physicsSystem->update(evt.timeSinceLastFrame);
}
// Update lights
if (m_lightSystem) {
m_lightSystem->update();
}
// Update cameras
if (m_cameraSystem) {
m_cameraSystem->update();
}
// Update LOD system
if (m_lodSystem) {
m_lodSystem->update();
}
// Update StaticGeometry system
if (m_staticGeometrySystem) {
m_staticGeometrySystem->update();
}
// Update ProceduralTexture system
if (m_proceduralTextureSystem) {
m_proceduralTextureSystem->update();
}
// Update ProceduralMaterial system
if (m_proceduralMaterialSystem) {
m_proceduralMaterialSystem->update();
}
// Update CharacterSlot system
/* --- Visual mesh setup (must run before animation) --- */
if (m_characterSlotSystem) {
m_characterSlotSystem->update();
}
// Update AnimationTree system
/* --- Animation / procedural generation --- */
if (m_animationTreeSystem) {
m_animationTreeSystem->update(evt.timeSinceLastFrame);
if (m_behaviorTreeSystem)
m_behaviorTreeSystem->update(evt.timeSinceLastFrame);
}
if (m_pathFollowingSystem) {
m_pathFollowingSystem->update(evt.timeSinceLastFrame);
}
// Update ProceduralMesh system
if (m_proceduralMeshSystem) {
m_proceduralMeshSystem->update();
}
// Update RoomLayout system FIRST (generates cells for CellGrid)
/* --- Static world generation (meshes + physics) --- */
if (m_roomLayoutSystem) {
m_roomLayoutSystem->update();
}
// Update CellGrid system (builds mesh from cells)
if (m_cellGridSystem) {
m_cellGridSystem->update();
}
/* --- Normal debug visualization (after geometry is built) --- */
if (m_normalDebugSystem) {
m_normalDebugSystem->update();
}
/* --- NavMesh builds after static geometry is ready --- */
if (m_navMeshSystem) {
m_navMeshSystem->update(evt.timeSinceLastFrame);
}
/* --- Smart Object system (AI navigation to smart objects) --- */
if (m_smartObjectSystem) {
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) {
m_characterSystem->update(evt.timeSinceLastFrame);
}
/* --- Buoyancy system (before physics so impulse is integrated) --- */
if (m_buoyancySystem) {
// Update camera position for water detection area
if (m_camera) {
Ogre::Vector3 cameraPos = m_camera->getPosition();
m_buoyancySystem->setCameraPosition(cameraPos);
}
m_buoyancySystem->update(evt.timeSinceLastFrame);
}
/* --- Main physics step --- */
if (m_physicsSystem) {
m_physicsSystem->update(evt.timeSinceLastFrame);
}
/* --- Rendering support systems --- */
if (m_sunSystem) {
m_sunSystem->update(evt.timeSinceLastFrame);
}
if (m_skyboxSystem) {
Ogre::Camera *cam = nullptr;
if (m_sceneMgr->hasCamera("PlayerCamera"))
cam = m_sceneMgr->getCamera("PlayerCamera");
if (!cam && m_camera)
cam = m_camera->getCamera();
m_skyboxSystem->update(cam);
}
if (m_waterPlaneSystem) {
Ogre::Camera *cam = nullptr;
if (m_sceneMgr->hasCamera("PlayerCamera"))
cam = m_sceneMgr->getCamera("PlayerCamera");
if (!cam && m_camera)
cam = m_camera->getCamera();
m_waterPlaneSystem->update(evt.timeSinceLastFrame, cam);
}
if (m_lightSystem) {
m_lightSystem->update();
}
if (m_cameraSystem) {
m_cameraSystem->update();
}
if (m_lodSystem) {
m_lodSystem->update();
}
if (m_staticGeometrySystem) {
m_staticGeometrySystem->update();
}
if (m_proceduralTextureSystem) {
m_proceduralTextureSystem->update();
}
if (m_proceduralMaterialSystem) {
m_proceduralMaterialSystem->update();
}
// Reset per-frame input state
m_gameInput.resetPerFrame();
// Don't call base class - it crashes when iterating input listeners
return true;
}
bool EditorApp::mouseMoved(const OgreBites::MouseMotionEvent &evt)
{
if (m_gameMode == GameMode::Game) {
m_gameInput.mouseMoved = true;
m_gameInput.mouseDeltaX += evt.xrel;
m_gameInput.mouseDeltaY += evt.yrel;
return true;
}
// Skip if ImGui wants to capture mouse (for gizmo, we still want to process even if over UI)
// But we need to update hover state
if (m_camera && m_uiSystem) {
@@ -524,6 +1049,10 @@ bool EditorApp::mouseMoved(const OgreBites::MouseMotionEvent &evt)
bool EditorApp::mousePressed(const OgreBites::MouseButtonEvent &evt)
{
if (m_gameMode == GameMode::Game) {
return true;
}
// Get mouse ray for gizmo interaction FIRST (before ImGui check)
// This allows clicking on 3D gizmo even when mouse is over empty UI areas
if (m_camera && m_uiSystem) {
@@ -550,6 +1079,10 @@ bool EditorApp::mousePressed(const OgreBites::MouseButtonEvent &evt)
bool EditorApp::mouseReleased(const OgreBites::MouseButtonEvent &evt)
{
if (m_gameMode == GameMode::Game) {
return true;
}
// Handle gizmo mouse release (always process to end dragging)
if (m_uiSystem) {
if (m_uiSystem->onMouseReleased()) {
@@ -569,6 +1102,42 @@ bool EditorApp::keyPressed(const OgreBites::KeyboardEvent &evt)
{
m_currentModifiers = evt.keysym.mod;
if (m_gameMode == GameMode::Game) {
bool pressed = true;
switch (evt.keysym.sym) {
case 'w':
case 'W':
m_gameInput.w = pressed;
break;
case 's':
case 'S':
m_gameInput.s = pressed;
break;
case 'a':
case 'A':
m_gameInput.a = pressed;
break;
case 'd':
case 'D':
m_gameInput.d = pressed;
break;
case OgreBites::SDLK_LSHIFT:
m_gameInput.shift = pressed;
break;
case 'e':
case 'E':
m_gameInput.e = pressed;
m_gameInput.ePressed = true;
break;
case 'f':
case 'F':
m_gameInput.f = pressed;
m_gameInput.fPressed = true;
break;
}
return true;
}
// Forward to camera for FPS movement
if (m_camera) {
m_camera->handleKeyboard(evt);
@@ -599,6 +1168,40 @@ bool EditorApp::keyReleased(const OgreBites::KeyboardEvent &evt)
{
m_currentModifiers = evt.keysym.mod;
if (m_gameMode == GameMode::Game) {
bool pressed = false;
switch (evt.keysym.sym) {
case 'w':
case 'W':
m_gameInput.w = pressed;
break;
case 's':
case 'S':
m_gameInput.s = pressed;
break;
case 'a':
case 'A':
m_gameInput.a = pressed;
break;
case 'd':
case 'D':
m_gameInput.d = pressed;
break;
case OgreBites::SDLK_LSHIFT:
m_gameInput.shift = pressed;
break;
case 'e':
case 'E':
m_gameInput.e = pressed;
break;
case 'f':
case 'F':
m_gameInput.f = pressed;
break;
}
return true;
}
// Forward to camera for FPS movement
if (m_camera) {
m_camera->handleKeyboard(evt);
@@ -630,4 +1233,4 @@ void EditorApp::locateResources()
Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
"./characters/female", "FileSystem", "Characters", false, true);
OgreBites::ApplicationContext::locateResources();
}
}

View File

@@ -9,6 +9,7 @@
#include <OgreRenderTargetListener.h>
#include <flecs.h>
#include <memory>
#include "lua/LuaState.hpp"
// Forward declarations
class EditorUISystem;
@@ -23,8 +24,54 @@ class ProceduralMaterialSystem;
class ProceduralMeshSystem;
class CharacterSlotSystem;
class AnimationTreeSystem;
class BehaviorTreeSystem;
class NavMeshSystem;
class CharacterSystem;
class CellGridSystem;
class RoomLayoutSystem;
class StartupMenuSystem;
class DialogueSystem;
class PlayerControllerSystem;
class BuoyancySystem;
class EditorSunSystem;
class EditorSkyboxSystem;
class EditorWaterPlaneSystem;
class NormalDebugSystem;
class SmartObjectSystem;
class GoapRunnerSystem;
class PathFollowingSystem;
class GoapPlannerSystem;
class ActuatorSystem;
class EventHandlerSystem;
class ItemSystem;
class EditorApp;
/**
* Shared input state for game mode
*/
struct GameInputState {
bool w = false;
bool a = false;
bool s = false;
bool d = false;
bool shift = false;
bool e = false;
bool f = false;
bool ePressed = false;
bool fPressed = false;
float mouseDeltaX = 0.0f;
float mouseDeltaY = 0.0f;
bool mouseMoved = false;
void resetPerFrame()
{
mouseMoved = false;
mouseDeltaX = 0.0f;
mouseDeltaY = 0.0f;
ePressed = false;
fPressed = false;
}
};
/**
* RenderTargetListener for ImGui frame management
@@ -34,7 +81,8 @@ class ImGuiRenderListener : public Ogre::RenderTargetListener {
public:
ImGuiRenderListener(Ogre::ImGuiOverlay *imguiOverlay,
EditorUISystem *uiSystem,
Ogre::RenderWindow *renderWindow);
Ogre::RenderWindow *renderWindow,
EditorApp *editorApp);
void
preViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override;
@@ -45,6 +93,7 @@ private:
Ogre::ImGuiOverlay *m_imguiOverlay;
EditorUISystem *m_uiSystem;
Ogre::RenderWindow *m_renderWindow;
EditorApp *m_editorApp;
// Timer for delta time calculation
Ogre::Timer m_timer;
@@ -56,11 +105,14 @@ private:
};
/**
* Main application class for the scene editor
* Main application class for the scene editor / game
*/
class EditorApp : public OgreBites::ApplicationContext,
public OgreBites::InputListener {
public:
enum class GameMode { Editor, Game };
enum class GamePlayState { Menu, Playing, Paused };
EditorApp();
virtual ~EditorApp();
@@ -85,6 +137,33 @@ public:
void setupECS();
void createDefaultEntities();
// Game mode management
void setGameMode(GameMode mode);
GameMode getGameMode() const
{
return m_gameMode;
}
// Debug buoyancy
void setDebugBuoyancy(bool enabled);
bool getDebugBuoyancy() const
{
return m_debugBuoyancy;
}
GamePlayState getGamePlayState() const
{
return m_gamePlayState;
}
void setGamePlayState(GamePlayState state);
void startNewGame(const Ogre::String &scenePath);
void clearScene();
// Input access
GameInputState &getGameInputState()
{
return m_gameInput;
}
// Getters
flecs::entity getSelectedEntity() const;
Ogre::SceneManager *getSceneManager() const
@@ -95,6 +174,38 @@ public:
{
return &m_world;
}
EditorCamera *getEditorCamera() const
{
return m_camera.get();
}
AnimationTreeSystem *getAnimationTreeSystem() const
{
return m_animationTreeSystem.get();
}
CharacterSlotSystem *getCharacterSlotSystem() const
{
return m_characterSlotSystem.get();
}
StartupMenuSystem *getStartupMenuSystem() const
{
return m_startupMenuSystem.get();
}
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;
}
private:
// Ogre objects
@@ -111,6 +222,10 @@ private:
std::unique_ptr<EditorCamera> m_camera;
std::unique_ptr<ImGuiRenderListener> m_imguiListener;
std::unique_ptr<EditorPhysicsSystem> m_physicsSystem;
std::unique_ptr<BuoyancySystem> m_buoyancySystem;
std::unique_ptr<EditorSunSystem> m_sunSystem;
std::unique_ptr<EditorSkyboxSystem> m_skyboxSystem;
std::unique_ptr<EditorWaterPlaneSystem> m_waterPlaneSystem;
std::unique_ptr<EditorLightSystem> m_lightSystem;
std::unique_ptr<EditorCameraSystem> m_cameraSystem;
std::unique_ptr<EditorLodSystem> m_lodSystem;
@@ -120,11 +235,39 @@ private:
std::unique_ptr<ProceduralMeshSystem> m_proceduralMeshSystem;
std::unique_ptr<CharacterSlotSystem> m_characterSlotSystem;
std::unique_ptr<AnimationTreeSystem> m_animationTreeSystem;
std::unique_ptr<BehaviorTreeSystem> m_behaviorTreeSystem;
std::unique_ptr<NavMeshSystem> m_navMeshSystem;
std::unique_ptr<CharacterSystem> m_characterSystem;
std::unique_ptr<CellGridSystem> m_cellGridSystem;
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
uint16_t m_currentModifiers;
GameMode m_gameMode = GameMode::Editor;
GamePlayState m_gamePlayState = GamePlayState::Menu;
GameInputState m_gameInput;
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;
};
#endif // EDITSCENE_EDITORAPP_HPP

View File

@@ -0,0 +1,37 @@
#include "GameMode.hpp"
namespace editScene
{
namespace
{
/// Global game mode state.
GameMode s_gameMode = GameMode::Editor;
/// Global gameplay state (only meaningful in game mode).
GamePlayState s_gamePlayState = GamePlayState::Menu;
} // anonymous namespace
void setEditSceneGameMode(GameMode mode) noexcept
{
s_gameMode = mode;
}
void setEditSceneGamePlayState(GamePlayState state) noexcept
{
s_gamePlayState = state;
}
GameMode getGameMode() noexcept
{
return s_gameMode;
}
GamePlayState getGamePlayState() noexcept
{
return s_gamePlayState;
}
} // namespace editScene

View File

@@ -0,0 +1,101 @@
#ifndef EDITSCENE_GAMEMODE_HPP
#define EDITSCENE_GAMEMODE_HPP
#pragma once
/**
* @file GameMode.hpp
*
* Global game mode query functions for the editScene feature.
*
* These functions allow any code in the editScene feature to query
* whether the application is currently in editor mode or game mode,
* and what the current gameplay state is, without needing a direct
* pointer to EditorApp.
*
* The EditorApp sets the current mode via setEditSceneGameMode()
* during its lifetime. Code outside the editScene feature should
* continue to use EditorApp::getGameMode() / getGamePlayState()
* directly.
*/
namespace editScene
{
/**
* Application mode: editor or game.
*/
enum class GameMode { Editor, Game };
/**
* Play state when in game mode.
*/
enum class GamePlayState { Menu, Playing, Paused };
// ---------------------------------------------------------------------------
// Global state management (called by EditorApp)
// ---------------------------------------------------------------------------
/**
* Set the current game mode. Called by EditorApp on mode changes.
*/
void setEditSceneGameMode(GameMode mode) noexcept;
/**
* Set the current gameplay state. Called by EditorApp on state changes.
*/
void setEditSceneGamePlayState(GamePlayState state) noexcept;
// ---------------------------------------------------------------------------
// Query functions
// ---------------------------------------------------------------------------
/**
* Return the current application mode.
*/
GameMode getGameMode() noexcept;
/**
* Return the current gameplay state (only meaningful in game mode).
*/
GamePlayState getGamePlayState() noexcept;
// ---------------------------------------------------------------------------
// Predicates
// ---------------------------------------------------------------------------
/** True when the application is in editor mode. */
inline bool isEditorMode() noexcept
{
return getGameMode() == GameMode::Editor;
}
/** True when the application is in game mode (any play state). */
inline bool isGameMode() noexcept
{
return getGameMode() == GameMode::Game;
}
/** True when in game mode and the gameplay state is Playing. */
inline bool isGamePlaying() noexcept
{
return getGameMode() == GameMode::Game &&
getGamePlayState() == GamePlayState::Playing;
}
/** True when in game mode and the gameplay state is Menu. */
inline bool isGameMenu() noexcept
{
return getGameMode() == GameMode::Game &&
getGamePlayState() == GamePlayState::Menu;
}
/** True when in game mode and the gameplay state is Paused. */
inline bool isGamePaused() noexcept
{
return getGameMode() == GameMode::Game &&
getGamePlayState() == GamePlayState::Paused;
}
} // namespace editScene
#endif // EDITSCENE_GAMEMODE_HPP

View File

@@ -15,8 +15,7 @@
*/
class EditorCamera {
public:
EditorCamera(Ogre::SceneManager *sceneMgr,
Ogre::RenderWindow *window);
EditorCamera(Ogre::SceneManager *sceneMgr, Ogre::RenderWindow *window);
~EditorCamera();
/**
@@ -35,7 +34,10 @@ public:
/**
* Get the camera
*/
Ogre::Camera *getCamera() const { return m_camera; }
Ogre::Camera *getCamera() const
{
return m_camera;
}
/**
* Focus camera on a point
@@ -47,6 +49,14 @@ public:
*/
void setPosition(const Ogre::Vector3 &pos);
/**
* Get camera position
*/
Ogre::Vector3 getPosition() const
{
return m_position;
}
/**
* Get ray from mouse position
*/
@@ -55,7 +65,10 @@ public:
/**
* Check if in FPS mode
*/
bool isFPSMode() const { return m_fpsMode; }
bool isFPSMode() const
{
return m_fpsMode;
}
private:
void updateCameraPosition();
@@ -65,7 +78,7 @@ private:
Ogre::Camera *m_camera;
Ogre::SceneNode *m_cameraNode;
Ogre::SceneNode *m_targetNode;
// Use OgreBites::CameraMan for proper camera control
std::unique_ptr<OgreBites::CameraMan> m_cameraMan;

View File

@@ -0,0 +1,515 @@
#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
{
for (const auto &action : actions) {
if (action.name == name)
return &action;
}
return nullptr;
}
GoapAction *ActionDatabase::findAction(const Ogre::String &name)
{
for (auto &action : actions) {
if (action.name == name)
return &action;
}
return nullptr;
}
const GoapGoal *ActionDatabase::findGoal(const Ogre::String &name) const
{
for (const auto &goal : goals) {
if (goal.name == name)
return &goal;
}
return nullptr;
}
GoapGoal *ActionDatabase::findGoal(const Ogre::String &name)
{
for (auto &goal : goals) {
if (goal.name == name)
return &goal;
}
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) {
if (it->name == name) {
actions.erase(it);
return true;
}
}
return false;
}
bool ActionDatabase::removeGoal(const Ogre::String &name)
{
for (auto it = goals.begin(); it != goals.end(); ++it) {
if (it->name == name) {
goals.erase(it);
return true;
}
}
return false;
}
// ---------------------------------------------------------------------------
// Selection / validation
// ---------------------------------------------------------------------------
const GoapGoal *
ActionDatabase::selectBestGoal(const GoapBlackboard &blackboard) const
{
const GoapGoal *best = nullptr;
int bestPriority = -1;
for (const auto &goal : goals) {
if (!goal.isValid(blackboard))
continue;
if (goal.priority > bestPriority) {
bestPriority = goal.priority;
best = &goal;
}
}
return best;
}
std::vector<const GoapAction *>
ActionDatabase::getValidActions(const GoapBlackboard &blackboard) const
{
std::vector<const GoapAction *> result;
for (const auto &action : actions) {
if (action.canRun(blackboard))
result.push_back(&action);
}
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();
});
}

View File

@@ -0,0 +1,114 @@
#ifndef EDITSCENE_ACTION_DATABASE_HPP
#define EDITSCENE_ACTION_DATABASE_HPP
#pragma once
#include "GoapAction.hpp"
#include "GoapGoal.hpp"
#include <vector>
#include <unordered_map>
#include <string>
// Forward declaration for reloadFromSceneComponents
namespace flecs
{
class world;
}
/**
* Global action database singleton.
*
* Holds the master list of GOAP actions and goals that characters can use.
* 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.
*/
class ActionDatabase {
public:
/** Get the singleton instance */
static ActionDatabase &getSingleton();
static ActionDatabase *getSingletonPtr();
std::vector<GoapAction> actions;
std::vector<GoapGoal> goals;
// Find an action by name
const GoapAction *findAction(const Ogre::String &name) const;
GoapAction *findAction(const Ogre::String &name);
// Find a goal by name
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);
// Remove a goal by name
bool removeGoal(const Ogre::String &name);
// Select the best valid goal for a given blackboard
// Returns nullptr if no valid goal exists
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;
// 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

View File

@@ -0,0 +1,47 @@
#include "ActionDatabase.hpp"
#include "ActionDebug.hpp"
#include "BehaviorTree.hpp"
#include "GoapAction.hpp"
#include "GoapGoal.hpp"
#include "GoapBlackboard.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/ActionDatabaseEditor.hpp"
#include "../ui/BehaviorTreeEditor.hpp"
REGISTER_COMPONENT_GROUP("Action Database", "AI", ActionDatabaseComponent,
ActionDatabaseEditor)
{
registry.registerComponent<ActionDatabaseComponent>(
"Action Database", "AI",
std::make_unique<ActionDatabaseEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<ActionDatabaseComponent>())
e.set<ActionDatabaseComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<ActionDatabaseComponent>())
e.remove<ActionDatabaseComponent>();
});
}
REGISTER_COMPONENT_GROUP("Behavior Tree", "AI", BehaviorTreeComponent,
BehaviorTreeEditor)
{
registry.registerComponent<BehaviorTreeComponent>(
"Behavior Tree", "AI", std::make_unique<BehaviorTreeEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<BehaviorTreeComponent>()) {
BehaviorTreeComponent bt;
bt.root.type = "sequence";
e.set<BehaviorTreeComponent>(bt);
}
},
// Remover
[](flecs::entity e) {
if (e.has<BehaviorTreeComponent>())
e.remove<BehaviorTreeComponent>();
});
}

View File

@@ -0,0 +1,39 @@
#ifndef EDITSCENE_ACTION_DEBUG_HPP
#define EDITSCENE_ACTION_DEBUG_HPP
#pragma once
#include "GoapBlackboard.hpp"
#include <Ogre.h>
#include <vector>
#include <unordered_map>
/**
* 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
GoapBlackboard blackboard;
// Currently selected action for test-running
Ogre::String selectedActionName;
// Currently selected goal for testing
Ogre::String selectedGoalName;
// Test-run state
bool isRunning = false;
float runTimer = 0.0f;
Ogre::String currentActionName;
// Debug output
Ogre::String lastResult;
ActionDebug() = default;
};
#endif // EDITSCENE_ACTION_DEBUG_HPP

View File

@@ -0,0 +1,21 @@
#include "ActionDebug.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/ActionDebugEditor.hpp"
REGISTER_COMPONENT_GROUP("Action Debug", "AI", ActionDebug,
ActionDebugEditor)
{
registry.registerComponent<ActionDebug>(
"Action Debug", "AI",
std::make_unique<ActionDebugEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<ActionDebug>())
e.set<ActionDebug>({});
},
// Remover
[](flecs::entity e) {
if (e.has<ActionDebug>())
e.remove<ActionDebug>();
});
}

View 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

View 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>();
});
}

View File

@@ -150,6 +150,12 @@ struct AnimationTreeComponent {
bool useRootMotion = false;
bool dirty = true;
/* If set, the tree root is copied from the named template */
Ogre::String templateName;
/* Runtime: last copied template version (not serialized) */
uint64_t templateVersion = 0;
/* Runtime: current state of each state machine (not serialized) */
std::unordered_map<Ogre::String, Ogre::String> currentStates;

View File

@@ -0,0 +1,20 @@
#ifndef EDITSCENE_ANIMATIONTREETEMPLATE_HPP
#define EDITSCENE_ANIMATIONTREETEMPLATE_HPP
#pragma once
#include <Ogre.h>
/**
* Template marker for reusable animation trees.
*
* Entities with this component serve as shared animation tree templates.
* They should also have an AnimationTreeComponent for editing the tree.
* Other entities reference the template by name via
* AnimationTreeComponent::templateName.
*/
struct AnimationTreeTemplate {
Ogre::String name;
uint64_t version = 1;
};
#endif // EDITSCENE_ANIMATIONTREETEMPLATE_HPP

View File

@@ -0,0 +1,23 @@
#include "AnimationTreeTemplate.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/AnimationTreeTemplateEditor.hpp"
REGISTER_COMPONENT_GROUP("Animation Tree Template", "Animation",
AnimationTreeTemplate, AnimationTreeTemplateEditor)
{
registry.registerComponent<AnimationTreeTemplate>(
AnimationTreeTemplate_name, AnimationTreeTemplate_group,
std::make_unique<AnimationTreeTemplateEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<AnimationTreeTemplate>()) {
e.set<AnimationTreeTemplate>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<AnimationTreeTemplate>()) {
e.remove<AnimationTreeTemplate>();
}
});
}

View File

@@ -0,0 +1,130 @@
#ifndef EDITSCENE_BEHAVIOR_TREE_HPP
#define EDITSCENE_BEHAVIOR_TREE_HPP
#pragma once
#include <Ogre.h>
#include <vector>
/**
* Data-driven behavior tree node for AI action execution.
*
* 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 action (references a named task)
* "check" - Leaf condition (references a named check)
* "debugPrint" - Leaf: prints 'name' to console once when active
* "setAnimationState"- Leaf: sets animation state (name="SM/State")
* "isAnimationEnded" - Leaf check: true if anim in state machine ended
* "setBit" - Leaf: sets blackboard bit (name=bit, params=0/1)
* "checkBit" - Leaf check: true if blackboard bit is set
* "setValue" - Leaf: sets blackboard value (name=key, params=val)
* "checkValue" - Leaf check: blackboard comparison (name=key, params="op val")
* "blackboardDump" - Leaf: dumps entire blackboard to log
* "delay" - Leaf: waits for N seconds (params=seconds as float)
* "teleportToChild" - Leaf: teleports character to a named child entity
* of the Smart Object being interacted with.
* name = child entity name to teleport to.
* The character is positioned at the child's absolute
* world transform (position + orientation).
* "disablePhysics" - Leaf: removes character's JPH::BodyID from physics
* 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";
Ogre::String name; // Action/condition name, or message, or SM/State
Ogre::String params; // Optional extra parameters
std::vector<BehaviorTreeNode> children;
BehaviorTreeNode() = default;
BehaviorTreeNode *findChild(const Ogre::String &childName)
{
for (auto &child : children) {
if (child.name == childName)
return &child;
}
return nullptr;
}
const BehaviorTreeNode *findChild(const Ogre::String &childName) const
{
for (const auto &child : children) {
if (child.name == childName)
return &child;
}
return nullptr;
}
bool canHaveChildren() const
{
return type == "sequence" || type == "selector" ||
type == "invert";
}
bool isLeaf() const
{
return type == "task" || type == "check" ||
type == "debugPrint" || type == "setAnimationState" ||
type == "isAnimationEnded" || type == "setBit" ||
type == "checkBit" || type == "setValue" ||
type == "checkValue" || type == "blackboardDump" ||
type == "delay" || type == "teleportToChild" ||
type == "disablePhysics" || type == "enablePhysics" ||
type == "sendEvent" || type == "hasItem" ||
type == "hasItemByName" || type == "countItem" ||
type == "pickupItem" || type == "dropItem" ||
type == "useItem" || type == "addItemToInventory" ||
type == "luaTask";
}
};
/**
* Behavior tree asset component.
*
* Can be attached to an entity to define a reusable behavior tree,
* or referenced by name from a GoapAction.
*/
struct BehaviorTreeComponent {
BehaviorTreeNode root;
Ogre::String treeName;
bool enabled = true;
bool dirty = true;
void markDirty()
{
dirty = true;
}
};
#endif // EDITSCENE_BEHAVIOR_TREE_HPP

View File

@@ -0,0 +1,46 @@
#ifndef EDITSCENE_BUOYANCYINFO_HPP
#define EDITSCENE_BUOYANCYINFO_HPP
#pragma once
#include <Ogre.h>
/**
* BuoyancyInfo component
* Provides per-entity buoyancy settings for water physics
* If an entity has this component, it will use these settings
* Otherwise, default settings from the buoyancy system will be used
*/
struct BuoyancyInfo {
// Enable/disable buoyancy for this entity
bool enabled = true;
// Buoyancy strength (0 = no buoyancy, 1 = neutral buoyancy, >1 = floats)
float buoyancy = 1.0f;
// Linear drag when submerged (0 = no drag, 1 = full drag)
float linearDrag = 0.1f;
// Angular drag when submerged (0 = no drag, 1 = full drag)
float angularDrag = 0.05f;
// Water surface Y level for this entity (world space)
// If not set (0), uses global water level from buoyancy system
float waterSurfaceY = 0.0f;
// Submergedness threshold (0-1) - how much of the body must be submerged
// before buoyancy is applied (0 = any contact, 1 = fully submerged)
float submergedThreshold = 0.3f;
// Use custom water surface level (if false, uses global water level)
bool useCustomWaterLevel = false;
// Mark component as dirty (needs update)
bool dirty = true;
void markDirty()
{
dirty = true;
}
};
#endif // EDITSCENE_BUOYANCYINFO_HPP

View File

@@ -0,0 +1,23 @@
#include "BuoyancyInfo.hpp"
#include "Transform.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/BuoyancyInfoEditor.hpp"
// Register BuoyancyInfo component
REGISTER_COMPONENT("Buoyancy Info", BuoyancyInfo, BuoyancyInfoEditor)
{
registry.registerComponent<BuoyancyInfo>(
"Buoyancy Info", std::make_unique<BuoyancyInfoEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<BuoyancyInfo>()) {
e.set<BuoyancyInfo>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<BuoyancyInfo>()) {
e.remove<BuoyancyInfo>();
}
});
}

View File

@@ -145,6 +145,9 @@ struct CellGridComponent {
std::string roofTopRectName;
std::string roofSideRectName;
// Physics properties for generated colliders
float friction = 0.5f;
// Dirty flag - triggers rebuild
bool dirty = true;
unsigned int version = 0;
@@ -419,6 +422,9 @@ struct LotComponent {
// Texture rectangle name from ProceduralTexture for UV mapping
std::string textureRectName;
// Physics properties for generated colliders
float friction = 0.5f;
// Dirty flag
bool dirty = true;
@@ -451,6 +457,9 @@ struct DistrictComponent {
// Texture rectangle name from ProceduralTexture for UV mapping
std::string textureRectName;
// Physics properties for generated colliders
float friction = 0.5f;
// Dirty flag
bool dirty = true;

View File

@@ -0,0 +1,61 @@
#ifndef EDITSCENE_CHARACTER_HPP
#define EDITSCENE_CHARACTER_HPP
#pragma once
#include <Ogre.h>
/**
* Character physics component
*
* Attaches a Jolt JPH::Character (kinematic capsule) to the entity.
* The entity may also have CharacterSlotsComponent; the character
* physics lives on the same entity as the visual character.
*
* Child entities can add extra collision shapes via PhysicsColliderComponent.
*/
struct CharacterComponent {
/* Capsule dimensions */
float radius = 0.3f;
float height = 1.8f; /* cylinder height (excluding spherical caps) */
/* Offset from the entity's scene node */
Ogre::Vector3 offset = Ogre::Vector3::ZERO;
/* Current linear velocity (m/s), applied each frame by CharacterSystem */
Ogre::Vector3 linearVelocity = Ogre::Vector3::ZERO;
/* Enable/disable physics character */
bool enabled = true;
/* Physics was explicitly disabled (e.g. by behavior tree node).
* When true, the character's JPH::BodyID is removed from the physics
* system but the JPH::Character object is kept alive so it can be
* re-added later. This is separate from 'enabled' which controls
* whether the character system processes this entity at all. */
bool physicsDisabled = false;
/* 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;
bool useGravity = true;
float getHalfHeight() const
{
return height * 0.5f;
}
float getTotalHeight() const
{
return height + 2.0f * radius;
}
};
#endif // EDITSCENE_CHARACTER_HPP

View File

@@ -0,0 +1,20 @@
#include "../ui/ComponentRegistration.hpp"
#include "Character.hpp"
#include "../ui/CharacterEditor.hpp"
REGISTER_COMPONENT_GROUP("Character Physics", "Physics",
CharacterComponent, CharacterEditor)
{
registry.registerComponent<CharacterComponent>(
"Character Physics", "Physics", std::make_unique<CharacterEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<CharacterComponent>())
e.set<CharacterComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<CharacterComponent>())
e.remove<CharacterComponent>();
});
}

View File

@@ -17,6 +17,13 @@ struct CharacterSlotsComponent {
/* Runtime: master entity with shared skeleton (set by CharacterSlotSystem) */
Ogre::Entity *masterEntity = nullptr;
/**
* Front-facing axis for this character model.
* Most models face -Z (NEGATIVE_UNIT_Z), but some face +Z.
* This is used by path following to rotate the character correctly.
*/
Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
CharacterSlotsComponent() = default;
};

View 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 (EventParams) parameters:
* "text" (string) - Narration text to display
* "choices" (string_array) - Array of choice label strings (Lua table)
* "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

View File

@@ -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>();
}
});
}

View 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
* (EventParams) are injected 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

View 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>();
});
}

View File

@@ -0,0 +1,739 @@
#ifndef EDITSCENE_EVENT_PARAMS_HPP
#define EDITSCENE_EVENT_PARAMS_HPP
#pragma once
#include <Ogre.h>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include <cassert>
/**
* @file EventParams.hpp
* @brief Tagged union type for event parameters.
*
* A C++11-compatible, RTTI-free tagged union that supports:
* - Entity ID (uint64_t)
* - Integer (int64_t)
* - Float (float)
* - Double (double)
* - String (std::string)
* - Array of entity IDs (std::vector<uint64_t>)
* - Array of integers (std::vector<int64_t>)
* - Array of floats (std::vector<float>)
* - Array of doubles (std::vector<double>)
* - Array of strings (std::vector<std::string>)
*
* Named parameters are stored as a map of string -> EventValue,
* where EventValue is a tagged union of the above types.
*/
// Forward declaration for friend function
struct lua_State;
namespace editScene
{
// ---------------------------------------------------------------------------
// EventValue: A single tagged-union value
// ---------------------------------------------------------------------------
struct EventValue {
enum Type {
NIL = 0,
ENTITY_ID,
INT,
FLOAT,
DOUBLE,
STRING,
ENTITY_ID_ARRAY,
INT_ARRAY,
FLOAT_ARRAY,
DOUBLE_ARRAY,
STRING_ARRAY
};
Type type;
union {
uint64_t asEntityId;
int64_t asInt;
float asFloat;
double asDouble;
};
// Heap-allocated data (strings and arrays)
// We use raw pointers to avoid std::unique_ptr (C++11 compatible)
std::string *strPtr;
void *arrayPtr; // points to std::vector<T>*
size_t arraySize;
EventValue()
: type(NIL)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(nullptr)
, arraySize(0)
{
}
explicit EventValue(uint64_t entityId)
: type(ENTITY_ID)
, asEntityId(entityId)
, strPtr(nullptr)
, arrayPtr(nullptr)
, arraySize(0)
{
}
explicit EventValue(int64_t val)
: type(INT)
, asInt(val)
, strPtr(nullptr)
, arrayPtr(nullptr)
, arraySize(0)
{
}
explicit EventValue(int val)
: type(INT)
, asInt(static_cast<int64_t>(val))
, strPtr(nullptr)
, arrayPtr(nullptr)
, arraySize(0)
{
}
explicit EventValue(float val)
: type(FLOAT)
, asFloat(val)
, strPtr(nullptr)
, arrayPtr(nullptr)
, arraySize(0)
{
}
explicit EventValue(double val)
: type(DOUBLE)
, asDouble(val)
, strPtr(nullptr)
, arrayPtr(nullptr)
, arraySize(0)
{
}
explicit EventValue(const std::string &val)
: type(STRING)
, asEntityId(0)
, strPtr(new std::string(val))
, arrayPtr(nullptr)
, arraySize(0)
{
}
explicit EventValue(const char *val)
: type(STRING)
, asEntityId(0)
, strPtr(new std::string(val ? val : ""))
, arrayPtr(nullptr)
, arraySize(0)
{
}
// Array constructors
explicit EventValue(const std::vector<uint64_t> &arr)
: type(ENTITY_ID_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<uint64_t>(arr))
, arraySize(arr.size())
{
}
explicit EventValue(const std::vector<int64_t> &arr)
: type(INT_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<int64_t>(arr))
, arraySize(arr.size())
{
}
explicit EventValue(const std::vector<int> &arr)
: type(INT_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<int64_t>(arr.begin(), arr.end()))
, arraySize(arr.size())
{
}
explicit EventValue(const std::vector<float> &arr)
: type(FLOAT_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<float>(arr))
, arraySize(arr.size())
{
}
explicit EventValue(const std::vector<double> &arr)
: type(DOUBLE_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<double>(arr))
, arraySize(arr.size())
{
}
explicit EventValue(const std::vector<std::string> &arr)
: type(STRING_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<std::string>(arr))
, arraySize(arr.size())
{
}
// Copy constructor
EventValue(const EventValue &other)
: type(other.type)
, asEntityId(other.asEntityId)
, strPtr(nullptr)
, arrayPtr(nullptr)
, arraySize(other.arraySize)
{
copyHeapData(other);
}
// Copy assignment
EventValue &operator=(const EventValue &other)
{
if (this != &other) {
destroyHeapData();
type = other.type;
asEntityId = other.asEntityId;
arraySize = other.arraySize;
strPtr = nullptr;
arrayPtr = nullptr;
copyHeapData(other);
}
return *this;
}
// Move constructor
EventValue(EventValue &&other) noexcept : type(other.type),
asEntityId(other.asEntityId),
strPtr(other.strPtr),
arrayPtr(other.arrayPtr),
arraySize(other.arraySize)
{
other.type = NIL;
other.strPtr = nullptr;
other.arrayPtr = nullptr;
other.arraySize = 0;
}
// Move assignment
EventValue &operator=(EventValue &&other) noexcept
{
if (this != &other) {
destroyHeapData();
type = other.type;
asEntityId = other.asEntityId;
strPtr = other.strPtr;
arrayPtr = other.arrayPtr;
arraySize = other.arraySize;
other.type = NIL;
other.strPtr = nullptr;
other.arrayPtr = nullptr;
other.arraySize = 0;
}
return *this;
}
~EventValue()
{
destroyHeapData();
}
// --- Accessors ---
Type getType() const
{
return type;
}
uint64_t getEntityId() const
{
assert(type == ENTITY_ID);
return asEntityId;
}
int64_t getInt() const
{
assert(type == INT);
return asInt;
}
float getFloat() const
{
assert(type == FLOAT);
return asFloat;
}
double getDouble() const
{
assert(type == DOUBLE);
return asDouble;
}
const std::string &getString() const
{
assert(type == STRING && strPtr != nullptr);
return *strPtr;
}
const std::vector<uint64_t> &getEntityIdArray() const
{
assert(type == ENTITY_ID_ARRAY && arrayPtr != nullptr);
return *static_cast<const std::vector<uint64_t> *>(arrayPtr);
}
const std::vector<int64_t> &getIntArray() const
{
assert(type == INT_ARRAY && arrayPtr != nullptr);
return *static_cast<const std::vector<int64_t> *>(arrayPtr);
}
const std::vector<float> &getFloatArray() const
{
assert(type == FLOAT_ARRAY && arrayPtr != nullptr);
return *static_cast<const std::vector<float> *>(arrayPtr);
}
const std::vector<double> &getDoubleArray() const
{
assert(type == DOUBLE_ARRAY && arrayPtr != nullptr);
return *static_cast<const std::vector<double> *>(arrayPtr);
}
const std::vector<std::string> &getStringArray() const
{
assert(type == STRING_ARRAY && arrayPtr != nullptr);
return *static_cast<const std::vector<std::string> *>(arrayPtr);
}
// --- Convenience: get numeric value as double ---
double asNumeric() const
{
switch (type) {
case INT:
return static_cast<double>(asInt);
case FLOAT:
return static_cast<double>(asFloat);
case DOUBLE:
return asDouble;
case ENTITY_ID:
return static_cast<double>(asEntityId);
default:
return 0.0;
}
}
// --- Equality ---
bool operator==(const EventValue &other) const
{
if (type != other.type)
return false;
switch (type) {
case NIL:
return true;
case ENTITY_ID:
return asEntityId == other.asEntityId;
case INT:
return asInt == other.asInt;
case FLOAT:
return asFloat == other.asFloat;
case DOUBLE:
return asDouble == other.asDouble;
case STRING:
return strPtr && other.strPtr &&
*strPtr == *other.strPtr;
case ENTITY_ID_ARRAY:
return arrayPtr && other.arrayPtr &&
getEntityIdArray() == other.getEntityIdArray();
case INT_ARRAY:
return arrayPtr && other.arrayPtr &&
getIntArray() == other.getIntArray();
case FLOAT_ARRAY:
return arrayPtr && other.arrayPtr &&
getFloatArray() == other.getFloatArray();
case DOUBLE_ARRAY:
return arrayPtr && other.arrayPtr &&
getDoubleArray() == other.getDoubleArray();
case STRING_ARRAY:
return arrayPtr && other.arrayPtr &&
getStringArray() == other.getStringArray();
}
return false;
}
bool operator!=(const EventValue &other) const
{
return !(*this == other);
}
private:
void copyHeapData(const EventValue &other)
{
if (other.type == STRING && other.strPtr) {
strPtr = new std::string(*other.strPtr);
} else if (other.type == ENTITY_ID_ARRAY && other.arrayPtr) {
arrayPtr = new std::vector<uint64_t>(
*static_cast<const std::vector<uint64_t> *>(
other.arrayPtr));
} else if (other.type == INT_ARRAY && other.arrayPtr) {
arrayPtr = new std::vector<int64_t>(
*static_cast<const std::vector<int64_t> *>(
other.arrayPtr));
} else if (other.type == FLOAT_ARRAY && other.arrayPtr) {
arrayPtr = new std::vector<float>(
*static_cast<const std::vector<float> *>(
other.arrayPtr));
} else if (other.type == DOUBLE_ARRAY && other.arrayPtr) {
arrayPtr = new std::vector<double>(
*static_cast<const std::vector<double> *>(
other.arrayPtr));
} else if (other.type == STRING_ARRAY && other.arrayPtr) {
arrayPtr = new std::vector<std::string>(
*static_cast<const std::vector<std::string> *>(
other.arrayPtr));
}
}
void destroyHeapData()
{
if (type == STRING) {
delete strPtr;
} else if (type == ENTITY_ID_ARRAY) {
delete static_cast<std::vector<uint64_t> *>(arrayPtr);
} else if (type == INT_ARRAY) {
delete static_cast<std::vector<int64_t> *>(arrayPtr);
} else if (type == FLOAT_ARRAY) {
delete static_cast<std::vector<float> *>(arrayPtr);
} else if (type == DOUBLE_ARRAY) {
delete static_cast<std::vector<double> *>(arrayPtr);
} else if (type == STRING_ARRAY) {
delete static_cast<std::vector<std::string> *>(
arrayPtr);
}
strPtr = nullptr;
arrayPtr = nullptr;
}
};
// ---------------------------------------------------------------------------
// EventParams: A map of named EventValue entries
// ---------------------------------------------------------------------------
class EventParams {
public:
EventParams() = default;
// --- Set values ---
void setEntityId(const std::string &key, uint64_t val)
{
m_values[key] = EventValue(val);
}
void setInt(const std::string &key, int64_t val)
{
m_values[key] = EventValue(val);
}
void setFloat(const std::string &key, float val)
{
m_values[key] = EventValue(val);
}
void setDouble(const std::string &key, double val)
{
m_values[key] = EventValue(val);
}
void setString(const std::string &key, const std::string &val)
{
m_values[key] = EventValue(val);
}
void setEntityIdArray(const std::string &key,
const std::vector<uint64_t> &val)
{
m_values[key] = EventValue(val);
}
void setIntArray(const std::string &key,
const std::vector<int64_t> &val)
{
m_values[key] = EventValue(val);
}
void setFloatArray(const std::string &key,
const std::vector<float> &val)
{
m_values[key] = EventValue(val);
}
void setDoubleArray(const std::string &key,
const std::vector<double> &val)
{
m_values[key] = EventValue(val);
}
void setStringArray(const std::string &key,
const std::vector<std::string> &val)
{
m_values[key] = EventValue(val);
}
// --- Get values ---
bool has(const std::string &key) const
{
return m_values.find(key) != m_values.end();
}
const EventValue *get(const std::string &key) const
{
auto it = m_values.find(key);
if (it != m_values.end())
return &it->second;
return nullptr;
}
EventValue *get(const std::string &key)
{
auto it = m_values.find(key);
if (it != m_values.end())
return &it->second;
return nullptr;
}
// --- Typed getters with defaults ---
uint64_t getEntityId(const std::string &key,
uint64_t defaultVal = 0) const
{
auto v = get(key);
if (v && v->getType() == EventValue::ENTITY_ID)
return v->getEntityId();
return defaultVal;
}
int64_t getInt(const std::string &key, int64_t defaultVal = 0) const
{
auto v = get(key);
if (v && v->getType() == EventValue::INT)
return v->getInt();
return defaultVal;
}
float getFloat(const std::string &key, float defaultVal = 0.0f) const
{
auto v = get(key);
if (v && v->getType() == EventValue::FLOAT)
return v->getFloat();
return defaultVal;
}
double getDouble(const std::string &key, double defaultVal = 0.0) const
{
auto v = get(key);
if (v && v->getType() == EventValue::DOUBLE)
return v->getDouble();
return defaultVal;
}
std::string getString(const std::string &key,
const std::string &defaultVal = "") const
{
auto v = get(key);
if (v && v->getType() == EventValue::STRING)
return v->getString();
return defaultVal;
}
// --- Remove ---
void remove(const std::string &key)
{
m_values.erase(key);
}
// --- Clear ---
void clear()
{
m_values.clear();
}
// --- Size ---
size_t size() const
{
return m_values.size();
}
bool empty() const
{
return m_values.empty();
}
// --- Iteration ---
typedef std::unordered_map<std::string, EventValue>::const_iterator
ConstIterator;
typedef std::unordered_map<std::string, EventValue>::iterator Iterator;
ConstIterator begin() const
{
return m_values.begin();
}
ConstIterator end() const
{
return m_values.end();
}
Iterator begin()
{
return m_values.begin();
}
Iterator end()
{
return m_values.end();
}
// --- Merge ---
void merge(const EventParams &other)
{
for (const auto &pair : other.m_values)
m_values[pair.first] = pair.second;
}
// --- Equality ---
bool operator==(const EventParams &other) const
{
return m_values == other.m_values;
}
bool operator!=(const EventParams &other) const
{
return !(*this == other);
}
// --- Dump for debugging ---
std::string dump() const
{
std::string result = "EventParams:\n";
for (const auto &pair : m_values) {
result += " " + pair.first + " = ";
switch (pair.second.getType()) {
case EventValue::NIL:
result += "nil";
break;
case EventValue::ENTITY_ID:
result += "entity:" +
std::to_string(
pair.second.getEntityId());
break;
case EventValue::INT:
result += std::to_string(pair.second.getInt());
break;
case EventValue::FLOAT:
result +=
std::to_string(pair.second.getFloat());
break;
case EventValue::DOUBLE:
result +=
std::to_string(pair.second.getDouble());
break;
case EventValue::STRING:
result += "'" + pair.second.getString() + "'";
break;
case EventValue::ENTITY_ID_ARRAY: {
result += "[";
const auto &arr =
pair.second.getEntityIdArray();
for (size_t i = 0; i < arr.size(); i++) {
if (i > 0)
result += ", ";
result += "e:" + std::to_string(arr[i]);
}
result += "]";
break;
}
case EventValue::INT_ARRAY: {
result += "[";
const auto &arr = pair.second.getIntArray();
for (size_t i = 0; i < arr.size(); i++) {
if (i > 0)
result += ", ";
result += std::to_string(arr[i]);
}
result += "]";
break;
}
case EventValue::FLOAT_ARRAY: {
result += "[";
const auto &arr = pair.second.getFloatArray();
for (size_t i = 0; i < arr.size(); i++) {
if (i > 0)
result += ", ";
result += std::to_string(arr[i]);
}
result += "]";
break;
}
case EventValue::DOUBLE_ARRAY: {
result += "[";
const auto &arr = pair.second.getDoubleArray();
for (size_t i = 0; i < arr.size(); i++) {
if (i > 0)
result += ", ";
result += std::to_string(arr[i]);
}
result += "]";
break;
}
case EventValue::STRING_ARRAY: {
result += "[";
const auto &arr = pair.second.getStringArray();
for (size_t i = 0; i < arr.size(); i++) {
if (i > 0)
result += ", ";
result += "'" + arr[i] + "'";
}
result += "]";
break;
}
}
result += "\n";
}
return result;
}
// Allow LuaEventApi to access m_values directly for efficiency
friend EventParams readEventParams(lua_State *L, int idx);
private:
std::unordered_map<std::string, EventValue> m_values;
};
} // namespace editScene
#endif // EDITSCENE_EVENT_PARAMS_HPP

View File

@@ -0,0 +1,74 @@
#ifndef EDITSCENE_GOAP_ACTION_HPP
#define EDITSCENE_GOAP_ACTION_HPP
#pragma once
#include "GoapBlackboard.hpp"
#include "BehaviorTree.hpp"
#include <Ogre.h>
/**
* A GOAP action definition.
*
* Actions live in the ActionDatabase and can be executed by any character.
* Each action has preconditions (required blackboard state),
* effects (resulting blackboard state), a cost, and a behavior tree.
*/
struct GoapAction {
Ogre::String name;
int cost = 1;
// GOAP preconditions and effects
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;
// Optional: reference to a named behavior tree asset
Ogre::String behaviorTreeName;
GoapAction() = default;
explicit GoapAction(const Ogre::String &name_, int cost_ = 1)
: name(name_)
, cost(cost_)
{
}
// Check if the given blackboard satisfies this action's preconditions.
// Only bits in preconditionMask are compared.
bool canRun(const GoapBlackboard &blackboard) const
{
// 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;
}
};
#endif // EDITSCENE_GOAP_ACTION_HPP

View File

@@ -0,0 +1,227 @@
#include "GoapBlackboard.hpp"
#include <cstdlib>
std::array<std::string, 64> &GoapBlackboard::getBitNameRegistry()
{
static std::array<std::string, 64> registry;
static bool initialized = false;
if (!initialized) {
for (auto &s : registry)
s.clear();
initialized = true;
}
return registry;
}
void GoapBlackboard::setBitName(int index, const std::string &name)
{
if (index < 0 || index >= 64)
return;
getBitNameRegistry()[index] = name;
}
const char *GoapBlackboard::getBitName(int index)
{
if (index < 0 || index >= 64)
return nullptr;
const auto &name = getBitNameRegistry()[index];
return name.empty() ? nullptr : name.c_str();
}
int GoapBlackboard::findBitByName(const std::string &name)
{
if (name.empty())
return -1;
const auto &registry = getBitNameRegistry();
for (int i = 0; i < 64; i++) {
if (registry[i] == name)
return i;
}
return -1;
}
bool GoapBlackboard::satisfies(const GoapBlackboard &other) const
{
// 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 (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
if ((other.bits & missingMask) != 0)
return false;
}
// Check values: for every key in other, our value must match
for (const auto &pair : other.values) {
auto it = values.find(pair.first);
if (it == values.end()) {
// If we don't have the key, only satisfy if target is 0
if (pair.second != 0)
return false;
} else if (it->second != pair.second) {
return false;
}
}
return true;
}
void GoapBlackboard::apply(const GoapBlackboard &other)
{
// Apply bit effects
bits = (bits & ~other.mask) | (other.bits & other.mask);
mask |= other.mask;
// Apply value effects
for (const auto &pair : other.values)
values[pair.first] = pair.second;
}
bool GoapBlackboard::getScalarValue(const std::string &key,
float &out) const
{
auto itf = floatValues.find(key);
if (itf != floatValues.end()) {
out = itf->second;
return true;
}
auto iti = values.find(key);
if (iti != values.end()) {
out = static_cast<float>(iti->second);
return true;
}
return false;
}
int GoapBlackboard::distanceTo(const GoapBlackboard &target,
bool ignoreValues) const
{
int distance = 0;
// 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 (within relevant mask)
uint64_t missingInUs = target.mask & ~mask & relevantMask;
distance += __builtin_popcountll(target.bits & missingInUs);
// 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)
return distance;
// Value differences (int only — planner ignores float/vec3)
for (const auto &pair : target.values) {
auto it = values.find(pair.first);
if (it == values.end())
distance += std::abs(pair.second);
else
distance += std::abs(it->second - pair.second);
}
// Values we have that target doesn't (may need to be cleared)
for (const auto &pair : values) {
if (target.values.find(pair.first) == target.values.end())
distance += std::abs(pair.second);
}
return distance;
}
Ogre::String GoapBlackboard::dump() const
{
Ogre::String result = "Blackboard:\n";
result += " Bits:\n";
for (int i = 0; i < 64; i++) {
if (hasBit(i)) {
const char *name = getBitName(i);
if (name)
result += " " + Ogre::String(name) + " = " +
(getBit(i) ? "true" : "false") + "\n";
else
result += " bit[" +
Ogre::StringConverter::toString(i) + "] = " +
(getBit(i) ? "true" : "false") + "\n";
}
}
if (!values.empty()) {
result += " Int values:\n";
for (const auto &pair : values)
result += " " + pair.first + " = " +
Ogre::StringConverter::toString(pair.second) +
"\n";
}
if (!floatValues.empty()) {
result += " Float values:\n";
for (const auto &pair : floatValues)
result += " " + pair.first + " = " +
Ogre::StringConverter::toString(pair.second) +
"\n";
}
if (!vec3Values.empty()) {
result += " Vec3 values:\n";
for (const auto &pair : vec3Values)
result += " " + pair.first + " = (" +
Ogre::StringConverter::toString(pair.second.x) +
", " +
Ogre::StringConverter::toString(pair.second.y) +
", " +
Ogre::StringConverter::toString(pair.second.z) +
")\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;
uint64_t m = mask;
while (m) {
int bit = __builtin_ctzll(m);
result.push_back(bit);
m &= m - 1;
}
return result;
}

View File

@@ -0,0 +1,236 @@
#ifndef EDITSCENE_GOAP_BLACKBOARD_HPP
#define EDITSCENE_GOAP_BLACKBOARD_HPP
#pragma once
#include <Ogre.h>
#include <array>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
/**
* Lightweight GOAP blackboard for action preconditions, effects,
* and per-character runtime state.
*
* Uses a 64-bit bitfield for fast boolean flag checks (used by GOAP planner).
* Supports int, float, and Vector3 values (int is used for preconditions/effects;
* float/vec3 are for behavior-tree-driven character state).
*/
struct GoapBlackboard {
// Boolean flags: 64 bits available. These are used by the GOAP planner.
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;
// Named float values — runtime character state
std::unordered_map<std::string, float> floatValues;
// 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 --- */
void setBit(int index, bool value)
{
if (index < 0 || index >= 64)
return;
uint64_t bit = 1ULL << index;
mask |= bit;
if (value)
bits |= bit;
else
bits &= ~bit;
}
bool getBit(int index) const
{
if (index < 0 || index >= 64)
return false;
return (bits >> index) & 1ULL;
}
bool hasBit(int index) const
{
if (index < 0 || index >= 64)
return false;
return (mask >> index) & 1ULL;
}
void clearBit(int index)
{
if (index < 0 || index >= 64)
return;
uint64_t bit = 1ULL << index;
mask &= ~bit;
bits &= ~bit;
}
/* --- Integer value accessors (backward compat) --- */
void setValue(const std::string &key, int value)
{
values[key] = value;
}
int getValue(const std::string &key, int defaultValue = 0) const
{
auto it = values.find(key);
if (it != values.end())
return it->second;
return defaultValue;
}
bool hasValue(const std::string &key) const
{
return values.find(key) != values.end();
}
void removeValue(const std::string &key)
{
values.erase(key);
}
/* --- Float value accessors --- */
void setFloatValue(const std::string &key, float value)
{
floatValues[key] = value;
}
float getFloatValue(const std::string &key,
float defaultValue = 0.0f) const
{
auto it = floatValues.find(key);
if (it != floatValues.end())
return it->second;
return defaultValue;
}
bool hasFloatValue(const std::string &key) const
{
return floatValues.find(key) != floatValues.end();
}
void removeFloatValue(const std::string &key)
{
floatValues.erase(key);
}
/* --- Vector3 value accessors --- */
void setVec3Value(const std::string &key, const Ogre::Vector3 &value)
{
vec3Values[key] = value;
}
Ogre::Vector3 getVec3Value(
const std::string &key,
const Ogre::Vector3 &defaultValue = Ogre::Vector3::ZERO) const
{
auto it = vec3Values.find(key);
if (it != vec3Values.end())
return it->second;
return defaultValue;
}
bool hasVec3Value(const std::string &key) const
{
return vec3Values.find(key) != vec3Values.end();
}
void removeVec3Value(const std::string &key)
{
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;
/* --- GOAP methods --- */
bool satisfies(const GoapBlackboard &other) const;
void apply(const GoapBlackboard &other);
int distanceTo(const GoapBlackboard &target,
bool ignoreValues = false) const;
/* --- Utility --- */
bool isValid() const
{
return mask != 0 || !values.empty() || !floatValues.empty() ||
!vec3Values.empty();
}
void clear()
{
bits = 0;
mask = 0;
values.clear();
floatValues.clear();
vec3Values.clear();
stringValues.clear();
}
Ogre::String dump() const;
// Bit naming: global registry for human-readable bit names
static std::array<std::string, 64> &getBitNameRegistry();
static void setBitName(int index, const std::string &name);
static const char *getBitName(int index);
static int findBitByName(const std::string &name);
// List all set bit indices
std::vector<int> getSetBits() const;
// Equality
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 &&
stringValues == other.stringValues;
}
bool operator!=(const GoapBlackboard &other) const
{
return !(*this == other);
}
};
#endif // EDITSCENE_GOAP_BLACKBOARD_HPP

View File

@@ -0,0 +1,21 @@
#include "GoapBlackboard.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/GoapBlackboardComponentEditor.hpp"
REGISTER_COMPONENT_GROUP("Blackboard", "AI", GoapBlackboard,
GoapBlackboardComponentEditor)
{
registry.registerComponent<GoapBlackboard>(
"Blackboard", "AI",
std::make_unique<GoapBlackboardComponentEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<GoapBlackboard>())
e.set<GoapBlackboard>({});
},
// Remover
[](flecs::entity e) {
if (e.has<GoapBlackboard>())
e.remove<GoapBlackboard>();
});
}

View File

@@ -0,0 +1,283 @@
#include "GoapExpression.hpp"
#include <cctype>
#include <cstdlib>
#include <cstring>
void GoapExpression::skipWhitespace()
{
while (*m_pos == ' ' || *m_pos == '\t' || *m_pos == '\n' ||
*m_pos == '\r')
m_pos++;
}
bool GoapExpression::match(const char *s)
{
skipWhitespace();
size_t len = strlen(s);
if (strncmp(m_pos, s, len) == 0) {
m_pos += len;
return true;
}
return false;
}
GoapExpression::Node *GoapExpression::parsePrimary()
{
skipWhitespace();
// Parenthesized expression
if (match("(")) {
Node *node = parseExpression();
if (!node)
return nullptr;
if (!match(")")) {
setError("Expected ')'");
delete node;
return nullptr;
}
return node;
}
// Integer literal
if (isdigit(*m_pos) || (*m_pos == '-' && isdigit(m_pos[1]))) {
bool negative = false;
if (*m_pos == '-') {
negative = true;
m_pos++;
}
int value = 0;
while (isdigit(*m_pos)) {
value = value * 10 + (*m_pos - '0');
m_pos++;
}
Node *node = new Node(Node::Value);
node->value = negative ? -value : value;
return node;
}
// Variable name
if (isalpha(*m_pos) || *m_pos == '_') {
std::string name;
while (isalnum(*m_pos) || *m_pos == '_') {
name += *m_pos;
m_pos++;
}
Node *node = new Node(Node::Variable);
node->name = name;
return node;
}
setError("Unexpected character in expression");
return nullptr;
}
GoapExpression::Node *GoapExpression::parseComparison()
{
Node *left = parsePrimary();
if (!left)
return nullptr;
skipWhitespace();
if (match("==")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::Equal);
node->left = left;
node->right = right;
return node;
} else if (match("!=")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::NotEqual);
node->left = left;
node->right = right;
return node;
} else if (match("<=")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::LessEqual);
node->left = left;
node->right = right;
return node;
} else if (match(">=")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::GreaterEqual);
node->left = left;
node->right = right;
return node;
} else if (match("<")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::Less);
node->left = left;
node->right = right;
return node;
} else if (match(">")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::Greater);
node->left = left;
node->right = right;
return node;
}
return left;
}
GoapExpression::Node *GoapExpression::parseNot()
{
skipWhitespace();
if (match("!")) {
Node *child = parseNot();
if (!child)
return nullptr;
Node *node = new Node(Node::Not);
node->left = child;
return node;
}
return parseComparison();
}
GoapExpression::Node *GoapExpression::parseAnd()
{
Node *left = parseNot();
if (!left)
return nullptr;
while (true) {
if (match("&&")) {
Node *right = parseNot();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::And);
node->left = left;
node->right = right;
left = node;
} else {
break;
}
}
return left;
}
GoapExpression::Node *GoapExpression::parseExpression()
{
Node *left = parseAnd();
if (!left)
return nullptr;
while (true) {
if (match("||")) {
Node *right = parseAnd();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::Or);
node->left = left;
node->right = right;
left = node;
} else {
break;
}
}
return left;
}
bool GoapExpression::parse(const char *expr)
{
clear();
m_expr = expr;
m_pos = expr;
m_root = parseExpression();
if (!m_root)
return false;
skipWhitespace();
if (*m_pos != '\0') {
setError("Unexpected trailing characters");
clear();
return false;
}
return true;
}
int GoapExpression::evalNode(Node *node, const GoapBlackboard &bb) const
{
if (!node)
return 0;
switch (node->type) {
case Node::Value:
return node->value;
case Node::Variable:
return bb.getValue(node->name, 0);
case Node::Equal:
return evalNode(node->left, bb) == evalNode(node->right, bb) ? 1 : 0;
case Node::NotEqual:
return evalNode(node->left, bb) != evalNode(node->right, bb) ? 1 : 0;
case Node::Less:
return evalNode(node->left, bb) < evalNode(node->right, bb) ? 1 : 0;
case Node::Greater:
return evalNode(node->left, bb) > evalNode(node->right, bb) ? 1 : 0;
case Node::LessEqual:
return evalNode(node->left, bb) <= evalNode(node->right, bb) ? 1 : 0;
case Node::GreaterEqual:
return evalNode(node->left, bb) >= evalNode(node->right, bb) ? 1 : 0;
case Node::And:
return evalNode(node->left, bb) && evalNode(node->right, bb) ? 1 : 0;
case Node::Or:
return evalNode(node->left, bb) || evalNode(node->right, bb) ? 1 : 0;
case Node::Not:
return !evalNode(node->left, bb) ? 1 : 0;
}
return 0;
}
bool GoapExpression::evaluate(const GoapBlackboard &blackboard) const
{
if (!m_root)
return false;
return evalNode(m_root, blackboard) != 0;
}
void GoapExpression::setError(const char *msg)
{
m_error = msg;
if (m_pos && m_expr) {
m_error += " at position ";
m_error += std::to_string(m_pos - m_expr);
m_error += " near \"";
m_error += std::string(m_pos, strnlen(m_pos, 20));
m_error += "\"";
}
}
void GoapExpression::clear()
{
delete m_root;
m_root = nullptr;
m_expr = nullptr;
m_pos = nullptr;
m_error.clear();
}

View File

@@ -0,0 +1,85 @@
#ifndef EDITSCENE_GOAP_EXPRESSION_HPP
#define EDITSCENE_GOAP_EXPRESSION_HPP
#pragma once
#include "GoapBlackboard.hpp"
#include <string>
/**
* Simple expression evaluator for GOAP goal conditions.
*
* Supports:
* - Variable names (looked up in blackboard values, default 0)
* - Integer literals
* - Comparisons: ==, !=, <, >, <=, >=
* - Boolean operators: &&, ||
* - Parentheses for grouping
* - Unary negation: !
*
* Example: "health > 20 && (hunger > 50 || have_food == 1)"
*/
class GoapExpression {
public:
GoapExpression() = default;
// Parse an expression string. Returns true on success.
bool parse(const char *expr);
// Evaluate the parsed expression against a blackboard.
// Returns false if expression was not parsed successfully.
bool evaluate(const GoapBlackboard &blackboard) const;
// Get last error message
const std::string &getError() const { return m_error; }
private:
struct Node {
enum Type {
Value, // integer literal
Variable, // blackboard variable name
Equal,
NotEqual,
Less,
Greater,
LessEqual,
GreaterEqual,
And,
Or,
Not
} type;
int value = 0; // for Value
std::string name; // for Variable
Node *left = nullptr;
Node *right = nullptr;
Node(Type t)
: type(t)
{
}
~Node()
{
delete left;
delete right;
}
};
const char *m_expr = nullptr;
const char *m_pos = nullptr;
std::string m_error;
Node *m_root = nullptr;
void skipWhitespace();
bool match(const char *s);
Node *parseExpression(); // ||
Node *parseAnd(); // &&
Node *parseNot(); // !
Node *parseComparison(); // ==, !=, <, >, <=, >=
Node *parsePrimary(); // value, variable, (expr)
int evalNode(Node *node, const GoapBlackboard &bb) const;
void setError(const char *msg);
void clear();
};
#endif // EDITSCENE_GOAP_EXPRESSION_HPP

View File

@@ -0,0 +1,24 @@
#include "GoapGoal.hpp"
#include "GoapExpression.hpp"
bool GoapGoal::isSatisfied(const GoapBlackboard &blackboard) const
{
return blackboard.satisfies(target);
}
bool GoapGoal::isValid(const GoapBlackboard &blackboard) const
{
// If already satisfied, not a valid goal to pursue
if (isSatisfied(blackboard))
return false;
// Evaluate condition if present
if (!condition.empty()) {
GoapExpression expr;
if (expr.parse(condition.c_str())) {
return expr.evaluate(blackboard);
}
}
return true;
}

View File

@@ -0,0 +1,43 @@
#ifndef EDITSCENE_GOAP_GOAL_HPP
#define EDITSCENE_GOAP_GOAL_HPP
#pragma once
#include "GoapBlackboard.hpp"
#include <Ogre.h>
/**
* A GOAP goal definition.
*
* Goals are selected based on priority and validity.
* The target blackboard defines the desired world state.
* The condition string provides additional runtime validity checks
* using a simple expression language against blackboard values.
*/
struct GoapGoal {
Ogre::String name;
int priority = 1;
// Target blackboard state to achieve
GoapBlackboard target;
// Optional condition expression (e.g. "health > 20 && hunger > 50")
// If empty, the goal is always considered for validity checking
Ogre::String condition;
GoapGoal() = default;
explicit GoapGoal(const Ogre::String &name_, int priority_ = 1)
: name(name_)
, priority(priority_)
{
}
// Check if the goal is already satisfied by the given blackboard
bool isSatisfied(const GoapBlackboard &blackboard) const;
// Check if the goal is valid for the given blackboard
// (condition evaluates to true and goal is not already satisfied)
bool isValid(const GoapBlackboard &blackboard) const;
};
#endif // EDITSCENE_GOAP_GOAL_HPP

View 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

View 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>();
});
}

View 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

View 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>();
});
}

View File

@@ -0,0 +1,12 @@
#ifndef EDITSCENE_INWATER_HPP
#define EDITSCENE_INWATER_HPP
#pragma once
/**
* Tag component indicating the entity is currently in water.
* Automatically added/removed by BuoyancySystem.
*/
struct InWater {
};
#endif // EDITSCENE_INWATER_HPP

View 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

View 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>();
});
}

View 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

View 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>();
});
}

View File

@@ -0,0 +1,59 @@
#ifndef EDITSCENE_NAVMESH_HPP
#define EDITSCENE_NAVMESH_HPP
#pragma once
#include <Ogre.h>
#include <cstdint>
/**
* Navigation mesh component.
*
* Attached to a single "manager" entity that owns the tiled navmesh
* for the entire scene. The NavMeshSystem collects geometry from
* static rigid bodies, StaticGeometry members, and entities with
* NavMeshGeometrySource to build the mesh.
*/
struct NavMeshComponent {
// Recast build parameters
float cellSize = 0.3f;
float cellHeight = 0.2f;
float agentHeight = 2.5f;
float agentRadius = 0.5f;
float agentMaxClimb = 1.0f;
float agentMaxSlope = 20.0f;
float edgeMaxLen = 12.0f;
float edgeMaxError = 1.3f;
float regionMinSize = 50.0f;
float regionMergeSize = 20.0f;
int tileSize = 48; // cells per tile
// Runtime flags
bool enabled = true;
bool debugDraw = false;
bool dirty = true;
// Partial rebuild tracking (not serialized)
bool needsPartialRebuild = false;
Ogre::AxisAlignedBox rebuildArea;
};
/**
* Component that forces an entity's geometry to be included in
* navmesh generation regardless of physics state.
*/
struct NavMeshGeometrySource {
bool include = true;
};
/**
* Component for entities that want pathfinding on this navmesh.
*/
struct NavMeshAgent {
Ogre::Vector3 targetPos = Ogre::Vector3::ZERO;
bool hasTarget = false;
bool pathPending = false;
std::vector<Ogre::Vector3> currentPath;
int pathIndex = 0;
};
#endif // EDITSCENE_NAVMESH_HPP

View File

@@ -0,0 +1,38 @@
#include "NavMesh.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/NavMeshEditor.hpp"
#include "../ui/NavMeshGeometrySourceEditor.hpp"
REGISTER_COMPONENT_GROUP("NavMesh", "Navigation", NavMeshComponent,
NavMeshEditor)
{
registry.registerComponent<NavMeshComponent>(
"NavMesh", "Navigation",
std::make_unique<NavMeshEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<NavMeshComponent>())
e.set<NavMeshComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<NavMeshComponent>())
e.remove<NavMeshComponent>();
});
}
REGISTER_COMPONENT_GROUP("NavMesh Geometry Source", "Navigation",
NavMeshGeometrySource, NavMeshGeometrySourceEditor)
{
registry.registerComponent<NavMeshGeometrySource>(
"NavMesh Geometry Source", "Navigation",
std::make_unique<NavMeshGeometrySourceEditor>(),
[](flecs::entity e) {
if (!e.has<NavMeshGeometrySource>())
e.set<NavMeshGeometrySource>({});
},
[](flecs::entity e) {
if (e.has<NavMeshGeometrySource>())
e.remove<NavMeshGeometrySource>();
});
}

View 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

View 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>();
});
}

View File

@@ -0,0 +1,42 @@
#ifndef EDITSCENE_PLAYERCONTROLLER_HPP
#define EDITSCENE_PLAYERCONTROLLER_HPP
#pragma once
#include <Ogre.h>
/**
* Player controller component.
* Only active in game mode. Editable in editor mode.
*/
struct PlayerControllerComponent {
enum CameraMode { TPS = 0, FPS = 1 };
int cameraMode = TPS;
Ogre::String targetCharacterName = "";
Ogre::String fpsBoneName = "Head";
float tpsDistance = 3.0f;
float tpsHeight = 2.0f;
float mouseSensitivity = 0.2f;
/* Animation state machine configuration */
Ogre::String locomotionStateMachine = "locomotion";
Ogre::String idleState = "idle";
Ogre::String walkState = "walking";
Ogre::String runState = "running";
/* Swim animation states (used when character is in water) */
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

View File

@@ -0,0 +1,24 @@
#include "../ui/ComponentRegistration.hpp"
#include "../ui/PlayerControllerEditor.hpp"
#include "PlayerController.hpp"
REGISTER_COMPONENT_GROUP("Player Controller", "Game",
PlayerControllerComponent, PlayerControllerEditor)
{
registry.registerComponent<PlayerControllerComponent>(
PlayerControllerComponent_name,
PlayerControllerComponent_group,
std::make_unique<PlayerControllerEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<PlayerControllerComponent>()) {
e.set<PlayerControllerComponent>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<PlayerControllerComponent>()) {
e.remove<PlayerControllerComponent>();
}
});
}

View File

@@ -0,0 +1,24 @@
#ifndef EDITSCENE_PREFABINSTANCE_HPP
#define EDITSCENE_PREFABINSTANCE_HPP
#pragma once
#include <string>
/**
* @brief Marks an entity as an instance of a prefab asset.
*
* The entity's TransformComponent acts as the world-space override
* for the prefab root. All other components (and the child subtree)
* are loaded from the prefab file at runtime and are NOT serialized
* with the main scene.
*/
struct PrefabInstanceComponent {
/** Path to the prefab JSON file (relative to working dir) */
std::string prefabPath;
/** Set to true once the prefab has been instantiated.
* Prevents double-instantiation on repeated resolve calls.
*/
bool instantiated = false;
};
#endif // EDITSCENE_PREFABINSTANCE_HPP

View File

@@ -3,6 +3,7 @@
#include <string>
#include <Ogre.h>
#include <flecs.h>
#include "ProceduralTexture.hpp"
/**
* @brief Component for creating procedural Ogre materials
@@ -11,32 +12,47 @@
* The material can be referenced by name for use with meshes/entities.
*/
struct ProceduralMaterialComponent {
// Material name for Ogre resource
std::string materialName;
// Reference to entity with ProceduralTextureComponent (for diffuse)
flecs::entity diffuseTextureEntity;
// Whether the material needs regeneration
bool dirty = true;
// Whether the material has been created
bool created = false;
// Pointer to the created Ogre material
Ogre::MaterialPtr ogreMaterial;
// Track texture version for automatic rebuild when texture changes
unsigned int textureVersion = 0;
// Material properties
float ambient[3] = {0.2f, 0.2f, 0.2f}; // Ambient color (RGB 0-1)
float diffuse[3] = {1.0f, 1.0f, 1.0f}; // Diffuse color multiplier (RGB 0-1)
float specular[3] = {0.0f, 0.0f, 0.0f}; // Specular color (RGB 0-1)
float shininess = 32.0f; // Specular shininess
float roughness = 0.5f; // Roughness (0-1, for PBR-style)
void markDirty() {
dirty = true;
}
// Unique identifier for persistent referencing across scene loads
std::string materialId;
// Material name for Ogre resource
std::string materialName;
// Persistent reference to texture by ID
std::string diffuseTextureId;
// Runtime reference to entity with ProceduralTextureComponent (for diffuse)
flecs::entity diffuseTextureEntity = flecs::entity::null();
// Whether the material needs regeneration
bool dirty = true;
// Whether the material has been created
bool created = false;
// Pointer to the created Ogre material
Ogre::MaterialPtr ogreMaterial;
// Track texture version for automatic rebuild when texture changes
unsigned int textureVersion = 0;
// Material properties
float ambient[3] = { 0.2f, 0.2f, 0.2f }; // Ambient color (RGB 0-1)
float diffuse[3] = { 1.0f, 1.0f,
1.0f }; // Diffuse color multiplier (RGB 0-1)
float specular[3] = { 0.0f, 0.0f, 0.0f }; // Specular color (RGB 0-1)
float shininess = 32.0f; // Specular shininess
float roughness = 0.5f; // Roughness (0-1, for PBR-style)
void markDirty()
{
dirty = true;
}
// Helper to check if texture reference is valid
bool hasValidTexture() const
{
return diffuseTextureEntity.is_alive() &&
diffuseTextureEntity.has<ProceduralTextureComponent>();
}
};

View File

@@ -2,6 +2,7 @@
#include "../ui/ComponentRegistration.hpp"
#include "../ui/ProceduralMaterialEditor.hpp"
#include <OgreMaterialManager.h>
#include <OgreRTShaderSystem.h>
// Register ProceduralMaterial component
REGISTER_COMPONENT_GROUP("Procedural Material", "Material", ProceduralMaterialComponent, ProceduralMaterialEditor)
@@ -22,6 +23,12 @@ REGISTER_COMPONENT_GROUP("Procedural Material", "Material", ProceduralMaterialCo
// Clean up Ogre material - wrap in try/catch since MaterialManager may be shutting down
if (material.ogreMaterial) {
try {
Ogre::RTShader::ShaderGenerator *shaderGen =
Ogre::RTShader::ShaderGenerator::getSingletonPtr();
if (shaderGen) {
shaderGen->removeAllShaderBasedTechniques(
*material.ogreMaterial);
}
if (Ogre::MaterialManager::getSingletonPtr()) {
Ogre::MaterialManager::getSingleton().remove(material.ogreMaterial);
}

View File

@@ -10,13 +10,26 @@
* @brief Information about a named rectangle in the texture atlas
*/
struct TextureRectInfo {
std::string name;
float u1, v1; // Top-left UV (0-1 range)
float u2, v2; // Bottom-right UV (0-1 range)
TextureRectInfo() : u1(0), v1(0), u2(1), v2(1) {}
TextureRectInfo(const std::string& n, float left, float top, float right, float bottom)
: name(n), u1(left), v1(top), u2(right), v2(bottom) {}
std::string name;
float u1, v1; // Top-left UV (0-1 range)
float u2, v2; // Bottom-right UV (0-1 range)
TextureRectInfo()
: u1(0)
, v1(0)
, u2(1)
, v2(1)
{
}
TextureRectInfo(const std::string &n, float left, float top,
float right, float bottom)
: name(n)
, u1(left)
, v1(top)
, u2(right)
, v2(bottom)
{
}
};
/**
@@ -27,129 +40,144 @@ struct TextureRectInfo {
* Also supports named rectangles for texture atlas/UV mapping.
*/
struct ProceduralTextureComponent {
// Texture name for Ogre resource
std::string textureName;
// Grid dimensions (default 10x10)
static constexpr int GRID_SIZE = 10;
static constexpr int RECT_COUNT = GRID_SIZE * GRID_SIZE;
// Colors for each rectangle (stored as float4: r, g, b, a)
std::array<float, RECT_COUNT * 4> colors;
// Named rectangles for texture atlas (name -> rect info)
std::map<std::string, TextureRectInfo> namedRects;
// Texture size (default 512x512)
int textureSize = 512;
// UV margin for texture mapping (default 0.01, range 0.01-0.025)
// Used to prevent color bleeding by adding padding around UV coordinates
float uvMargin = 0.01f;
// Whether the texture needs regeneration
bool dirty = true;
// Whether the texture has been generated
bool generated = false;
// Version counter - increments each time texture is regenerated
// Used by dependent systems to detect texture changes
unsigned int version = 0;
// Pointer to the generated Ogre texture
Ogre::TexturePtr ogreTexture;
ProceduralTextureComponent() {
// Initialize with default colors (checkerboard pattern)
for (int i = 0; i < RECT_COUNT; ++i) {
int row = i / GRID_SIZE;
int col = i % GRID_SIZE;
bool isWhite = (row + col) % 2 == 0;
colors[i * 4 + 0] = isWhite ? 1.0f : 0.0f; // R
colors[i * 4 + 1] = isWhite ? 1.0f : 0.0f; // G
colors[i * 4 + 2] = isWhite ? 1.0f : 0.0f; // B
colors[i * 4 + 3] = 1.0f; // A
}
}
// Get color for a specific rectangle
Ogre::ColourValue getColor(int index) const {
if (index < 0 || index >= RECT_COUNT) return Ogre::ColourValue::White;
return Ogre::ColourValue(
colors[index * 4 + 0],
colors[index * 4 + 1],
colors[index * 4 + 2],
colors[index * 4 + 3]
);
}
// Set color for a specific rectangle
void setColor(int index, const Ogre::ColourValue& color) {
if (index < 0 || index >= RECT_COUNT) return;
colors[index * 4 + 0] = color.r;
colors[index * 4 + 1] = color.g;
colors[index * 4 + 2] = color.b;
colors[index * 4 + 3] = color.a;
dirty = true;
}
// Calculate UV coordinates for a rectangle index
void getRectUVs(int index, float& u1, float& v1, float& u2, float& v2) const {
int row = index / GRID_SIZE;
int col = index % GRID_SIZE;
float cellSize = 1.0f / GRID_SIZE;
u1 = col * cellSize;
v1 = row * cellSize;
u2 = (col + 1) * cellSize;
v2 = (row + 1) * cellSize;
}
// Add a named rectangle
bool addNamedRect(const std::string& name, int rectIndex) {
if (name.empty() || rectIndex < 0 || rectIndex >= RECT_COUNT) return false;
float u1, v1, u2, v2;
getRectUVs(rectIndex, u1, v1, u2, v2);
namedRects[name] = TextureRectInfo(name, u1, v1, u2, v2);
dirty = true; // Mark texture as dirty since rect atlas changed
return true;
}
// Remove a named rectangle
bool removeNamedRect(const std::string& name) {
auto it = namedRects.find(name);
if (it != namedRects.end()) {
namedRects.erase(it);
dirty = true; // Mark texture as dirty since rect atlas changed
return true;
}
return false;
}
// Get named rectangle info
const TextureRectInfo* getNamedRect(const std::string& name) const {
auto it = namedRects.find(name);
if (it != namedRects.end()) {
return &it->second;
}
return nullptr;
}
// Check if a name exists
bool hasNamedRect(const std::string& name) const {
return namedRects.find(name) != namedRects.end();
}
// Get all named rectangles
const std::map<std::string, TextureRectInfo>& getAllNamedRects() const {
return namedRects;
}
void markDirty() {
dirty = true;
}
// Unique identifier for persistent referencing across scene loads
std::string textureId;
// Texture name for Ogre resource
std::string textureName;
// Grid dimensions (default 10x10)
static constexpr int GRID_SIZE = 10;
static constexpr int RECT_COUNT = GRID_SIZE * GRID_SIZE;
// Colors for each rectangle (stored as float4: r, g, b, a)
std::array<float, RECT_COUNT * 4> colors;
// Named rectangles for texture atlas (name -> rect info)
std::map<std::string, TextureRectInfo> namedRects;
// Texture size (default 512x512)
int textureSize = 512;
// UV margin for texture mapping (default 0.01, range 0.01-0.025)
// Used to prevent color bleeding by adding padding around UV coordinates
float uvMargin = 0.01f;
// Whether the texture needs regeneration
bool dirty = true;
// Whether the texture has been generated
bool generated = false;
// Version counter - increments each time texture is regenerated
// Used by dependent systems to detect texture changes
unsigned int version = 0;
// Pointer to the generated Ogre texture
Ogre::TexturePtr ogreTexture;
ProceduralTextureComponent()
{
// Initialize with default colors (checkerboard pattern)
for (int i = 0; i < RECT_COUNT; ++i) {
int row = i / GRID_SIZE;
int col = i % GRID_SIZE;
bool isWhite = (row + col) % 2 == 0;
colors[i * 4 + 0] = isWhite ? 1.0f : 0.0f; // R
colors[i * 4 + 1] = isWhite ? 1.0f : 0.0f; // G
colors[i * 4 + 2] = isWhite ? 1.0f : 0.0f; // B
colors[i * 4 + 3] = 1.0f; // A
}
}
// Get color for a specific rectangle
Ogre::ColourValue getColor(int index) const
{
if (index < 0 || index >= RECT_COUNT)
return Ogre::ColourValue::White;
return Ogre::ColourValue(colors[index * 4 + 0],
colors[index * 4 + 1],
colors[index * 4 + 2],
colors[index * 4 + 3]);
}
// Set color for a specific rectangle
void setColor(int index, const Ogre::ColourValue &color)
{
if (index < 0 || index >= RECT_COUNT)
return;
colors[index * 4 + 0] = color.r;
colors[index * 4 + 1] = color.g;
colors[index * 4 + 2] = color.b;
colors[index * 4 + 3] = color.a;
dirty = true;
}
// Calculate UV coordinates for a rectangle index
void getRectUVs(int index, float &u1, float &v1, float &u2,
float &v2) const
{
int row = index / GRID_SIZE;
int col = index % GRID_SIZE;
float cellSize = 1.0f / GRID_SIZE;
u1 = col * cellSize;
v1 = row * cellSize;
u2 = (col + 1) * cellSize;
v2 = (row + 1) * cellSize;
}
// Add a named rectangle
bool addNamedRect(const std::string &name, int rectIndex)
{
if (name.empty() || rectIndex < 0 || rectIndex >= RECT_COUNT)
return false;
float u1, v1, u2, v2;
getRectUVs(rectIndex, u1, v1, u2, v2);
namedRects[name] = TextureRectInfo(name, u1, v1, u2, v2);
dirty = true; // Mark texture as dirty since rect atlas changed
return true;
}
// Remove a named rectangle
bool removeNamedRect(const std::string &name)
{
auto it = namedRects.find(name);
if (it != namedRects.end()) {
namedRects.erase(it);
dirty = true; // Mark texture as dirty since rect atlas changed
return true;
}
return false;
}
// Get named rectangle info
const TextureRectInfo *getNamedRect(const std::string &name) const
{
auto it = namedRects.find(name);
if (it != namedRects.end()) {
return &it->second;
}
return nullptr;
}
// Check if a name exists
bool hasNamedRect(const std::string &name) const
{
return namedRects.find(name) != namedRects.end();
}
// Get all named rectangles
const std::map<std::string, TextureRectInfo> &getAllNamedRects() const
{
return namedRects;
}
void markDirty()
{
dirty = true;
}
};

View File

@@ -0,0 +1,58 @@
#ifndef EDITSCENE_SKYBOX_HPP
#define EDITSCENE_SKYBOX_HPP
#pragma once
#include <Ogre.h>
/**
* Skybox component - procedural sky rendered as a large cube
* with a fragment shader that creates dynamic day/night/sunset
* sky gradients.
*
* Designed to work alongside SunComponent on the same entity.
* If no SunComponent is present, uses default noon lighting.
*/
struct SkyboxComponent {
// Enable/disable skybox
bool enabled = true;
// Size of the skybox cube (default 500)
float size = 500.0f;
// Day sky colors
Ogre::ColourValue dayTopColor = Ogre::ColourValue(0.2f, 0.5f, 1.0f);
Ogre::ColourValue dayBottomColor = Ogre::ColourValue(0.6f, 0.8f, 1.0f);
// Night sky colors
Ogre::ColourValue nightTopColor = Ogre::ColourValue(0.0f, 0.0f, 0.05f);
Ogre::ColourValue nightBottomColor = Ogre::ColourValue(0.05f, 0.05f, 0.15f);
// Horizon glow colors
Ogre::ColourValue sunriseColor = Ogre::ColourValue(1.0f, 0.5f, 0.2f);
Ogre::ColourValue sunsetColor = Ogre::ColourValue(1.0f, 0.3f, 0.1f);
// Angular size of sun/moon discs in the sky shader (0.01 - 0.2)
float sunSize = 0.05f;
float moonSize = 0.03f;
// Enable simple stars at night
bool starsEnabled = true;
// Cloud coverage (0.0 = clear, 1.0 = overcast)
// Not yet implemented in shader but reserved for future
float cloudiness = 0.0f;
// Runtime objects (managed by EditorSkyboxSystem)
Ogre::SceneNode *sceneNode = nullptr;
Ogre::ManualObject *manualObject = nullptr;
// Dirty flag - triggers rebuild
bool dirty = true;
void markDirty()
{
dirty = true;
}
};
#endif // EDITSCENE_SKYBOX_HPP

View File

@@ -0,0 +1,24 @@
#include "Skybox.hpp"
#include "Transform.hpp"
#include "EditorMarker.hpp"
#include "EntityName.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/SkyboxEditor.hpp"
REGISTER_COMPONENT_GROUP("Skybox", "Environment", SkyboxComponent, SkyboxEditor)
{
registry.registerComponent<SkyboxComponent>(
"Skybox", "Environment", std::make_unique<SkyboxEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<SkyboxComponent>()) {
e.set<SkyboxComponent>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<SkyboxComponent>()) {
e.remove<SkyboxComponent>();
}
});
}

View File

@@ -0,0 +1,38 @@
#ifndef EDITSCENE_SMART_OBJECT_HPP
#define EDITSCENE_SMART_OBJECT_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <string>
/**
* Smart Object component.
*
* Defines an interactive object in the world that characters can
* navigate to and perform GOAP actions on.
*
* The entity's TransformComponent defines the position/orientation.
* Characters will pathfind to within `radius` distance in XZ plane
* and `height` difference in Y, then execute the selected action.
*/
struct SmartObjectComponent {
// Interaction radius in XZ plane
float radius = 1.0f;
// Maximum height difference for interaction
float height = 1.8f;
// Names of GOAP actions (from ActionDatabase) that this object provides
std::vector<Ogre::String> actionNames;
SmartObjectComponent() = default;
explicit SmartObjectComponent(float radius_, float height_)
: radius(radius_)
, height(height_)
{
}
};
#endif // EDITSCENE_SMART_OBJECT_HPP

View File

@@ -0,0 +1,20 @@
#include "SmartObject.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/SmartObjectEditor.hpp"
REGISTER_COMPONENT_GROUP("Smart Object", "AI", SmartObjectComponent,
SmartObjectEditor)
{
registry.registerComponent<SmartObjectComponent>(
"Smart Object", "AI", std::make_unique<SmartObjectEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<SmartObjectComponent>())
e.set<SmartObjectComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<SmartObjectComponent>())
e.remove<SmartObjectComponent>();
});
}

View File

@@ -0,0 +1,20 @@
#ifndef EDITSCENE_STARTUPMENU_HPP
#define EDITSCENE_STARTUPMENU_HPP
#pragma once
#include <Ogre.h>
/**
* Configurable startup menu component.
* Only active in game mode. Editable in editor mode.
*/
struct StartupMenuComponent {
Ogre::String fontName = "Kenney Bold.ttf";
float fontSize = 36.0f;
Ogre::String newGameScene = "scene.json";
bool showLoadGame = true;
bool showOptions = true;
bool showQuit = true;
};
#endif // EDITSCENE_STARTUPMENU_HPP

View File

@@ -0,0 +1,23 @@
#include "../ui/ComponentRegistration.hpp"
#include "../ui/StartupMenuEditor.hpp"
#include "StartupMenu.hpp"
REGISTER_COMPONENT_GROUP("Startup Menu", "Game", StartupMenuComponent,
StartupMenuEditor)
{
registry.registerComponent<StartupMenuComponent>(
StartupMenuComponent_name, StartupMenuComponent_group,
std::make_unique<StartupMenuEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<StartupMenuComponent>()) {
e.set<StartupMenuComponent>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<StartupMenuComponent>()) {
e.remove<StartupMenuComponent>();
}
});
}

View File

@@ -0,0 +1,70 @@
#ifndef EDITSCENE_SUN_HPP
#define EDITSCENE_SUN_HPP
#pragma once
#include <Ogre.h>
/**
* Sun component - manages a directional light that rotates
* based on in-game time, with sun/moon visualization.
*
* Designed to work alongside SkyboxComponent on the same entity.
*/
struct SunComponent {
// Enable/disable sun system
bool enabled = true;
// Time of day in hours (0.0 - 24.0)
float timeOfDay = 12.0f;
// Game time speed: game-hours per real-second
// 0.01 = 1 game hour per 100 real seconds (slow)
// 0.1 = 1 game hour per 10 real seconds
// 1.0 = 1 game hour per 1 real second (fast)
float timeSpeed = 0.05f;
// Sun color when at zenith (day)
Ogre::ColourValue sunColor = Ogre::ColourValue(1.0f, 0.95f, 0.8f);
// Moon color when sun is below horizon (night)
Ogre::ColourValue moonColor = Ogre::ColourValue(0.3f, 0.3f, 0.5f);
// Ambient light colors
Ogre::ColourValue ambientDay = Ogre::ColourValue(0.3f, 0.3f, 0.3f);
Ogre::ColourValue ambientNight = Ogre::ColourValue(0.05f, 0.05f, 0.15f);
Ogre::ColourValue ambientSunrise = Ogre::ColourValue(0.3f, 0.2f, 0.15f);
Ogre::ColourValue ambientSunset = Ogre::ColourValue(0.25f, 0.15f, 0.1f);
// Sun / moon visualization spheres
bool showSunSphere = true;
bool showMoonSphere = true;
float sunSphereSize = 5.0f;
float moonSphereSize = 3.0f;
// Orbit tilt (degrees) - tilts the sun path north/south
float orbitTilt = 15.0f;
// Light intensity multiplier
float intensity = 1.0f;
// Cast shadows
bool castShadows = true;
// Runtime objects (managed by EditorSunSystem)
Ogre::Light *light = nullptr;
Ogre::SceneNode *lightNode = nullptr;
Ogre::SceneNode *sunSphereNode = nullptr;
Ogre::SceneNode *moonSphereNode = nullptr;
Ogre::ManualObject *sunSphere = nullptr;
Ogre::ManualObject *moonSphere = nullptr;
// Dirty flag - triggers rebuild
bool dirty = true;
void markDirty()
{
dirty = true;
}
};
#endif // EDITSCENE_SUN_HPP

View File

@@ -0,0 +1,24 @@
#include "Sun.hpp"
#include "Transform.hpp"
#include "EditorMarker.hpp"
#include "EntityName.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/SunEditor.hpp"
REGISTER_COMPONENT_GROUP("Sun", "Environment", SunComponent, SunEditor)
{
registry.registerComponent<SunComponent>(
"Sun", "Environment", std::make_unique<SunEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<SunComponent>()) {
e.set<SunComponent>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<SunComponent>()) {
e.remove<SunComponent>();
}
});
}

View File

@@ -0,0 +1,43 @@
#ifndef WATER_PHYSICS_HPP
#define WATER_PHYSICS_HPP
#include <Ogre.h>
struct WaterPhysics {
// Water surface Y level (world space)
float waterSurfaceY = -0.1f;
// Default buoyancy parameters (used when entity has no BuoyancyInfo)
float defaultBuoyancy = 1.5f; // 1.0 = neutral, >1 floats (Jolt convention)
float defaultLinearDrag = 0.5f; // Jolt recommends ~0.5
float defaultAngularDrag = 0.01f; // Jolt recommends ~0.01
float defaultSubmergedThreshold =
0.1f; // Minimum submerged fraction to apply buoyancy
// Water density (kg/m³)
float waterDensity = 1000.0f;
// Gravity acceleration (m/s²)
float gravity = 9.81f;
// Enable/disable water physics globally
bool enabled = true;
WaterPhysics() = default;
WaterPhysics(float surfaceY, float buoyancy, float linearDrag,
float angularDrag, float submergedThreshold, float density,
float grav, bool enable)
: waterSurfaceY(surfaceY)
, defaultBuoyancy(buoyancy)
, defaultLinearDrag(linearDrag)
, defaultAngularDrag(angularDrag)
, defaultSubmergedThreshold(submergedThreshold)
, waterDensity(density)
, gravity(grav)
, enabled(enable)
{
}
};
#endif // WATER_PHYSICS_HPP

View File

@@ -0,0 +1,22 @@
#include "WaterPhysics.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/WaterPhysicsEditor.hpp"
// Register WaterPhysics component
REGISTER_COMPONENT("Water Physics", WaterPhysics, WaterPhysicsEditor)
{
registry.registerComponent<WaterPhysics>(
"Water Physics", std::make_unique<WaterPhysicsEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<WaterPhysics>()) {
e.set<WaterPhysics>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<WaterPhysics>()) {
e.remove<WaterPhysics>();
}
});
}

View File

@@ -0,0 +1,73 @@
#ifndef EDITSCENE_WATERPLANE_HPP
#define EDITSCENE_WATERPLANE_HPP
#pragma once
#include <Ogre.h>
/**
* WaterPlane component
* Visual water surface with reflection/refraction
* for the editScene editor. Lightweight and OpenGL ES 2.0 compatible.
*/
struct WaterPlane {
// Enable/disable water
bool enabled = true;
// Water surface Y level (world space)
float waterSurfaceY = -0.1f;
// Plane size (width and depth)
float planeSize = 500.0f;
// Whether to update waterSurfaceY from WaterPhysics component
bool autoUpdateFromWaterPhysics = true;
// Visual settings
Ogre::ColourValue waterColor = Ogre::ColourValue(0.0f, 0.3f, 0.5f,
0.8f);
float reflectivity = 0.5f;
float waveSpeed = 1.0f;
float waveScale = 0.02f;
float tiling = 0.012f;
// Render texture size (power of two recommended)
int renderTextureSize = 512;
// Runtime objects (managed by EditorWaterPlaneSystem)
Ogre::SceneNode *sceneNode = nullptr;
Ogre::ManualObject *manualObject = nullptr;
// Render-to-texture resources
Ogre::TexturePtr renderTexture;
Ogre::Camera *reflectionCamera = nullptr;
Ogre::Camera *refractionCamera = nullptr;
Ogre::Viewport *reflectionViewport = nullptr;
Ogre::Viewport *refractionViewport = nullptr;
Ogre::RenderTarget *renderTarget = nullptr;
// Clip planes
Ogre::Plane reflectionPlane;
Ogre::Plane reflectionClipPlane;
Ogre::Plane refractionClipPlane;
// Camera following state
Ogre::Vector3 lastCameraPos = Ogre::Vector3::ZERO;
float positionUpdateTimer = 0.0f;
static constexpr float POSITION_UPDATE_INTERVAL = 0.3f;
// Which viewport to update this frame (0=reflection, 1=refraction)
int updateViewportIndex = 0;
// Time accumulator for shader
float shaderTime = 0.0f;
// Dirty flag
bool dirty = true;
void markDirty()
{
dirty = true;
}
};
#endif // EDITSCENE_WATERPLANE_HPP

View File

@@ -0,0 +1,25 @@
#include "WaterPlane.hpp"
#include "Transform.hpp"
#include "EditorMarker.hpp"
#include "EntityName.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/WaterPlaneEditor.hpp"
REGISTER_COMPONENT_GROUP("Water Plane", "Water", WaterPlane, WaterPlaneEditor)
{
registry.registerComponent<WaterPlane>(
"Water Plane", "Water",
std::make_unique<WaterPlaneEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<WaterPlane>()) {
e.set<WaterPlane>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<WaterPlane>()) {
e.remove<WaterPlane>();
}
});
}

View File

@@ -0,0 +1,409 @@
#include "Cursor3D.hpp"
#include "../components/Transform.hpp"
#include <cmath>
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_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_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_axisX);
m_cursorNode->attachObject(m_axisY);
m_cursorNode->attachObject(m_axisZ);
m_cursorNode->attachObject(m_centerMarker);
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();
}
void Cursor3D::shutdown()
{
if (!m_sceneMgr)
return;
auto detach = [&](Ogre::ManualObject *obj) {
if (m_cursorNode && obj) {
try {
m_cursorNode->detachObject(obj);
} catch (...) {
}
}
};
auto destroy = [&](Ogre::ManualObject *obj) {
if (obj) {
try {
m_sceneMgr->destroyManualObject(obj);
} catch (...) {
}
}
};
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);
} catch (...) {
}
m_cursorNode = nullptr;
}
m_sceneMgr = nullptr;
}
Cursor3D::~Cursor3D()
{
if (m_sceneMgr)
shutdown();
}
void Cursor3D::setPosition(const Ogre::Vector3 &pos)
{
m_position = pos;
updateNodeTransform();
}
void Cursor3D::setOrientation(const Ogre::Quaternion &rot)
{
m_orientation = rot;
updateNodeTransform();
}
void Cursor3D::setVisible(bool visible)
{
m_visible = visible;
if (m_cursorNode)
m_cursorNode->setVisible(visible);
}
bool Cursor3D::isVisible() const
{
return m_visible;
}
void Cursor3D::setSize(float size)
{
m_size = size;
createGeometry();
}
void Cursor3D::snapToTransform(const TransformComponent &transform)
{
if (transform.node) {
m_position = transform.node->_getDerivedPosition();
m_orientation = transform.node->_getDerivedOrientation();
} else {
m_position = transform.position;
m_orientation = transform.rotation;
}
updateNodeTransform();
}
void Cursor3D::applyToTransform(TransformComponent &transform) const
{
if (!transform.node)
return;
Ogre::SceneNode *parent = static_cast<Ogre::SceneNode *>(
transform.node->getParent());
if (parent) {
transform.position = parent->convertWorldToLocalPosition(
m_position);
transform.rotation = parent->convertWorldToLocalOrientation(
m_orientation);
} else {
transform.position = m_position;
transform.rotation = m_orientation;
}
transform.applyToNode();
transform.markChanged();
}
void Cursor3D::updateNodeTransform()
{
if (m_cursorNode) {
m_cursorNode->setPosition(m_position);
m_cursorNode->setOrientation(m_orientation);
}
}
void Cursor3D::createGeometry()
{
float len = 0.5f * m_size;
float half = 0.05f * m_size;
// 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();
// 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();
// 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();
// 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),
Ogre::Vector3(-half, half, -half),
Ogre::Vector3(-half, -half, half),
Ogre::Vector3(half, -half, half),
Ogre::Vector3(half, half, half),
Ogre::Vector3(-half, half, half),
};
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;
}

View File

@@ -0,0 +1,96 @@
#ifndef EDITSCENE_CURSOR3D_HPP
#define EDITSCENE_CURSOR3D_HPP
#pragma once
#include <Ogre.h>
#include <OgreManualObject.h>
// Forward declarations
struct TransformComponent;
/**
* 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();
void shutdown();
void setPosition(const Ogre::Vector3 &pos);
Ogre::Vector3 getPosition() const { return m_position; }
void setOrientation(const Ogre::Quaternion &rot);
Ogre::Quaternion getOrientation() const { return m_orientation; }
void setVisible(bool visible);
bool isVisible() const;
void setSize(float size);
float getSize() const { return m_size; }
void setMode(Mode mode) { m_mode = mode; }
Mode getMode() const { return m_mode; }
void snapToTransform(const TransformComponent &transform);
void applyToTransform(TransformComponent &transform) const;
/**
* Handle mouse input for cursor interaction.
* Returns true if cursor handled the input.
*/
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_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

View 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
-- =============================================================================

View 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.
-- =============================================================================

View 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!")

View File

@@ -0,0 +1,28 @@
--[[
debug_crash_example.lua
Demonstrates the ecs.debug_crash() function which prints a message
and then deliberately crashes the application via std::abort().
Usage:
ecs.debug_crash("message") -- prints message and crashes
ecs.debug_crash() -- uses default message "debug_crash called"
WARNING: This function will terminate the application!
]]
-- Example 1: Crash with a custom message
-- Uncomment to test:
-- ecs.debug_crash("Something went terribly wrong!")
-- Example 2: Crash with default message
-- Uncomment to test:
-- ecs.debug_crash()
-- Example 3: Conditional crash for debugging
-- local health = ecs.get_field(player_id, 'Character', 'health')
-- if health <= 0 then
-- ecs.debug_crash("Player health dropped to zero!")
-- end
print("debug_crash example loaded (not executed - uncomment to test)")

View File

@@ -0,0 +1,102 @@
-- =============================================================================
-- 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" (table) - Array of choice label strings (optional)
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 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", {
text = "Welcome to the world of World2!",
speaker = "Narrator"
})
print("Sent basic narration dialogue")
-- ---------------------------------------------------------------------------
-- 3. Show dialogue with player choices
-- ---------------------------------------------------------------------------
-- When "choices" is provided as a table, the dialogue box shows
-- buttons instead of click-to-progress. The player must pick one.
ecs.send_event("dialogue_show", {
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", {
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", {
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", { text = "...", speaker = "...", choices = { ... } })
-- 3. Required: text = "The narration text"
-- 4. Optional: speaker = "Speaker Name"
-- 5. Optional: choices = { "Choice1", "Choice2", "Choice3" } (table of strings)
-- 6. EventParams uses flat key-value pairs (no nested stringValues/floatValues/etc.)
-- 7. Type metadata is available via params._types table
-- =============================================================================
print("Dialogue basic show examples completed!")

View File

@@ -0,0 +1,219 @@
-- =============================================================================
-- 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", {
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
-- 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", {
text = base_text,
speaker = ecs.get_field(npc_entity, "Dialogue", "speaker"),
choices = choice_list
})
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 table of strings
-- - 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!")

View File

@@ -0,0 +1,260 @@
-- =============================================================================
-- 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.
--
-- Event parameters use the EventParams type with flat key-value pairs.
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 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", {
npc_name = npc_name,
location = "town_square",
distance = distance
})
-- Also show dialogue directly
ecs.send_event("dialogue_show", {
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.choice_index or 0
local choice_text = params.choice_text or ""
if choice_text == "I'll help!" then
-- Player accepted the quest - trigger quest acceptance event
ecs.send_event("quest_accepted", {
quest_name = "The Lost Artifact",
giver = "QuestGiver",
reward_gold = 100,
reward_xp = 500
})
-- Show follow-up dialogue
ecs.send_event("dialogue_show", {
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", {
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", {
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.quest_name or "unknown"
local reward = params.reward_gold or 0
local xp = params.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", {
quest_name = quest_name,
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", {
text = dialogue.text,
speaker = dialogue.speaker
})
-- Also send a zone-specific event for other systems
ecs.send_event("zone_entered", {
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", {
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
--
-- EventParams uses flat key-value pairs. Type metadata is available
-- via params._types table (e.g., params._types.reward_gold = "int").
-- =============================================================================
print("Dialogue EventHandler integration examples completed!")

View File

@@ -0,0 +1,143 @@
-- =============================================================================
-- 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)
--
-- Event parameters use the EventParams type, which supports flat
-- key-value pairs with typed values. Use params._types to check types.
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 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.choice_index or 0
local choice_text = params.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.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.text or ""
local speaker = params.speaker or "Unknown"
local choices = params.choices or {}
print("[Dialogue Log] " .. speaker .. ": \"" .. text .. "\"")
if #choices > 0 then
print("[Dialogue Log] Choices: " .. table.concat(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", {
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", {
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.choice_index (1-based) to see which was picked
-- 3. Check params.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
--
-- EventParams uses flat key-value pairs. Type metadata is available
-- via params._types table (e.g., params._types.choice_index = "int").
-- =============================================================================
print("Dialogue event subscription examples completed!")

View File

@@ -0,0 +1,320 @@
-- =============================================================================
-- 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.choice_index or 0
dialogue_queue_choice_text = params.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 table|nil Array of choice label strings (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", {
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", {
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", {
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 table from the node's choices
local choices = {}
local choice_map = {}
if node.choices then
for i, choice in ipairs(node.choices) do
table.insert(choices, choice.text)
choice_map[i] = choice
end
end
-- Show the dialogue
ecs.send_event("dialogue_show", {
text = node.text,
speaker = node.speaker or "",
choices = choices
})
-- 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", {
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", {
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", {
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", {
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!")

View 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!")

View File

@@ -0,0 +1,296 @@
-- =============================================================================
-- 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.
--
-- Event parameters use flat key-value pairs (no nested tables like
-- stringValues/floatValues/values). Each value is typed automatically:
-- - integer -> int
-- - number (non-integer) -> double
-- - string -> string
-- - table of integers -> int_array
-- - table of numbers -> double_array
-- - table of strings -> string_array
-- - table of entity IDs -> entity_id_array
--
-- Type metadata is available via params._types table:
-- params._types.key = "int" | "double" | "string" | "int_array" |
-- "double_array" | "string_array" | "entity_id_array"
-- =============================================================================
-- =============================================================================
-- 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 (flat key-value pairs):
ecs.send_event("hello", {
count = 42,
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
print(" Damage: " .. (params.damage or 0))
print(" Health remaining: " .. (params.health or 0))
print(" Source: " .. (params.source or "unknown"))
local pos = params.position
if pos then
print(" Position: " .. pos[1] .. ", " .. pos[2] .. ", " .. pos[3])
end
end
end)
-- Send a damage event:
ecs.send_event("player_damaged", {
damage = 25,
health = 75,
source = "goblin_archer",
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.
-- All values are flat keys with automatic type inference:
ecs.subscribe_event("data_event", function(event, params)
print("Received data_event with:")
if params then
-- Print all keys with their types
for k, v in pairs(params) do
if k ~= "_types" then
local t = type(v)
if t == "table" then
local arr_str = "{"
for i, elem in ipairs(v) do
if i > 1 then arr_str = arr_str .. ", " end
arr_str = arr_str .. tostring(elem)
end
arr_str = arr_str .. "}"
print(" " .. k .. " (array) = " .. arr_str)
else
print(" " .. k .. " (" .. t .. ") = " .. tostring(v))
end
end
end
-- Print type metadata
if params._types then
print(" Type metadata:")
for k, t in pairs(params._types) do
print(" " .. k .. " -> " .. t)
end
end
end
end)
-- Send an event with all parameter types:
ecs.send_event("data_event", {
score = 100,
level = 5,
kills = 42,
speed = 1.5,
health = 75.5,
name = "Hero",
state = "exploring",
position = { 10, 20, 30 },
velocity = { 1, 0, 0 },
tags = { "warrior", "human", "player" }
})
-- =============================================================================
-- 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.quest_name or "unknown"
local reward_xp = params and params.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.enemy_type or "unknown"
local xp = params and params.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.item_name or "unknown"
local count = params and params.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.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.hour or 0
local minute = params and params.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.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", {
quest_name = "The Lost Artifact",
reward_xp = 500
})
ecs.send_event("enemy_killed", {
enemy_type = "Goblin Warrior",
xp_reward = 50
})
ecs.send_event("item_picked_up", {
item_name = "Health Potion",
count = 2
})
ecs.send_event("dialogue_started", {
npc_name = "Elder Marcus"
})
ecs.send_event("time_changed", {
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 flat key-value pairs:
-- key = integer -> stored as int
-- key = number -> stored as double
-- key = string -> stored as string
-- key = {int, ...} -> stored as int_array
-- key = {num, ...} -> stored as double_array
-- key = {str, ...} -> stored as string_array
-- Type metadata is available via params._types table:
-- params._types.key = "int" | "double" | "string" |
-- "int_array" | "double_array" | "string_array"
-- =============================================================================
print("Event API examples completed successfully!")

View File

@@ -0,0 +1,135 @@
-- =============================================================================
-- Game Mode Lua API Examples
-- =============================================================================
-- This file demonstrates how to query the editor/game mode state from Lua
-- using the ecs.* Lua API.
--
-- The application can be in one of two modes:
-- - Editor mode: the scene editor is active
-- - Game mode: the game is running (with sub-states: menu, playing, paused)
--
-- These functions allow Lua scripts to adapt their behavior based on the
-- current application mode.
-- =============================================================================
-- =============================================================================
-- Querying the Current Mode
-- =============================================================================
-- Check if we are in editor mode:
if ecs.is_editor_mode() then
print("Application is in EDITOR mode")
end
-- Check if we are in game mode (any play state):
if ecs.is_game_mode() then
print("Application is in GAME mode")
end
-- Get the mode as a string:
local mode = ecs.get_game_mode()
print("Current mode: " .. mode) -- "editor" or "game"
-- =============================================================================
-- Querying the Gameplay State (only meaningful in game mode)
-- =============================================================================
-- Check specific gameplay states:
if ecs.is_game_playing() then
print("Game is PLAYING")
end
if ecs.is_game_menu() then
print("Game is in MENU")
end
if ecs.is_game_paused() then
print("Game is PAUSED")
end
-- Get the play state as a string:
local state = ecs.get_game_play_state()
print("Game play state: " .. state) -- "menu", "playing", or "paused"
-- =============================================================================
-- Practical Examples
-- =============================================================================
-- Example 1: Only run editor-specific logic in editor mode
function update_editor_ui()
if ecs.is_editor_mode() then
-- Show editor UI elements
print("Updating editor UI...")
end
end
-- Example 2: Only process game input when game is playing
function process_game_input()
if ecs.is_game_playing() then
-- Process player input
print("Processing game input...")
end
end
-- Example 3: Show/hide pause menu
function toggle_pause_menu()
if ecs.is_game_paused() then
-- Show pause menu overlay
print("Showing pause menu...")
else
-- Hide pause menu
print("Hiding pause menu...")
end
end
-- Example 4: Conditional behavior based on mode
function on_entity_clicked(entity_id)
if ecs.is_editor_mode() then
-- In editor: select the entity
print("Selected entity " .. entity_id .. " in editor")
elseif ecs.is_game_playing() then
-- In game: interact with the entity
print("Interacting with entity " .. entity_id)
end
end
-- =============================================================================
-- Using Mode Queries in Event Handlers
-- =============================================================================
-- Register an event handler that checks mode:
function on_frame_update()
if ecs.is_game_playing() then
-- Update game logic
elseif ecs.is_editor_mode() then
-- Update editor logic
end
end
-- =============================================================================
-- Error Handling
-- =============================================================================
-- All functions return valid values even if called at unexpected times:
local m = ecs.get_game_mode()
assert(type(m) == "string", "get_game_mode should return a string")
local s = ecs.get_game_play_state()
assert(type(s) == "string", "get_game_play_state should return a string")
local b1 = ecs.is_editor_mode()
assert(type(b1) == "boolean", "is_editor_mode should return a boolean")
local b2 = ecs.is_game_mode()
assert(type(b2) == "boolean", "is_game_mode should return a boolean")
local b3 = ecs.is_game_playing()
assert(type(b3) == "boolean", "is_game_playing should return a boolean")
local b4 = ecs.is_game_menu()
assert(type(b4) == "boolean", "is_game_menu should return a boolean")
local b5 = ecs.is_game_paused()
assert(type(b5) == "boolean", "is_game_paused should return a boolean")
print("Game mode API examples completed successfully!")

View 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

View 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

View 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 &params)
{
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 &params)
{
// 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 &params)
{
// 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

View 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 &params);
/**
* @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 &params);
} // namespace editScene
#endif // EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View File

@@ -0,0 +1,471 @@
#include "LuaEventApi.hpp"
#include "LuaEntityApi.hpp"
#include "../systems/EventBus.hpp"
#include "../components/EventParams.hpp"
#include <OgreLogManager.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 an EventValue as a Lua value
// ---------------------------------------------------------------------------
/**
* @brief Push a single EventValue onto the Lua stack.
*
* @param L Lua state.
* @param val The EventValue to push.
*/
static void pushEventValue(lua_State *L, const EventValue &val)
{
switch (val.getType()) {
case EventValue::NIL:
lua_pushnil(L);
break;
case EventValue::ENTITY_ID:
lua_pushinteger(L, (lua_Integer)val.getEntityId());
break;
case EventValue::INT:
lua_pushinteger(L, (lua_Integer)val.getInt());
break;
case EventValue::FLOAT:
lua_pushnumber(L, (lua_Number)val.getFloat());
break;
case EventValue::DOUBLE:
lua_pushnumber(L, (lua_Number)val.getDouble());
break;
case EventValue::STRING:
lua_pushstring(L, val.getString().c_str());
break;
case EventValue::ENTITY_ID_ARRAY: {
lua_newtable(L);
const auto &arr = val.getEntityIdArray();
for (size_t i = 0; i < arr.size(); i++) {
lua_pushinteger(L, (lua_Integer)arr[i]);
lua_rawseti(L, -2, (int)(i + 1));
}
break;
}
case EventValue::INT_ARRAY: {
lua_newtable(L);
const auto &arr = val.getIntArray();
for (size_t i = 0; i < arr.size(); i++) {
lua_pushinteger(L, (lua_Integer)arr[i]);
lua_rawseti(L, -2, (int)(i + 1));
}
break;
}
case EventValue::FLOAT_ARRAY: {
lua_newtable(L);
const auto &arr = val.getFloatArray();
for (size_t i = 0; i < arr.size(); i++) {
lua_pushnumber(L, (lua_Number)arr[i]);
lua_rawseti(L, -2, (int)(i + 1));
}
break;
}
case EventValue::DOUBLE_ARRAY: {
lua_newtable(L);
const auto &arr = val.getDoubleArray();
for (size_t i = 0; i < arr.size(); i++) {
lua_pushnumber(L, (lua_Number)arr[i]);
lua_rawseti(L, -2, (int)(i + 1));
}
break;
}
case EventValue::STRING_ARRAY: {
lua_newtable(L);
const auto &arr = val.getStringArray();
for (size_t i = 0; i < arr.size(); i++) {
lua_pushstring(L, arr[i].c_str());
lua_rawseti(L, -2, (int)(i + 1));
}
break;
}
}
}
/**
* @brief Push the type name string for an EventValue type.
*/
static const char *eventValueTypeName(EventValue::Type type)
{
switch (type) {
case EventValue::NIL:
return "nil";
case EventValue::ENTITY_ID:
return "entity_id";
case EventValue::INT:
return "int";
case EventValue::FLOAT:
return "float";
case EventValue::DOUBLE:
return "double";
case EventValue::STRING:
return "string";
case EventValue::ENTITY_ID_ARRAY:
return "entity_id_array";
case EventValue::INT_ARRAY:
return "int_array";
case EventValue::FLOAT_ARRAY:
return "float_array";
case EventValue::DOUBLE_ARRAY:
return "double_array";
case EventValue::STRING_ARRAY:
return "string_array";
}
return "nil";
}
// ---------------------------------------------------------------------------
// Helper: push an EventParams as a Lua table
// ---------------------------------------------------------------------------
/**
* @brief Push an EventParams as a Lua table with named fields.
*
* The resulting table has each key mapped to its value, plus a _types
* sub-table that maps each key to its type name string.
*
* @param L Lua state.
* @param params The EventParams to convert.
*/
static void pushEventParams(lua_State *L, const EventParams &params)
{
lua_newtable(L);
// Push _types sub-table
lua_newtable(L);
for (EventParams::ConstIterator it = params.begin(); it != params.end();
++it) {
const std::string &key = it->first;
const EventValue &val = it->second;
// Push the value
pushEventValue(L, val);
lua_setfield(L, -3, key.c_str());
// Push the type name into _types
lua_pushstring(L, eventValueTypeName(val.getType()));
lua_setfield(L, -2, key.c_str());
}
// Set _types sub-table
lua_setfield(L, -2, "_types");
}
// ---------------------------------------------------------------------------
// Helper: read a Lua value as an EventValue
// ---------------------------------------------------------------------------
/**
* @brief Read a Lua value at the given index as an EventValue.
*
* Type inference rules:
* - integer -> INT
* - number (non-integer) -> DOUBLE
* - string -> STRING
* - table with all integer keys -> array (type inferred from elements)
* - table with string keys -> not supported at value level
*
* @param L Lua state.
* @param idx Stack index of the value.
* @return EventValue representing the Lua value.
*/
static EventValue readLuaValue(lua_State *L, int idx)
{
int absIdx = lua_absindex(L, idx);
int type = lua_type(L, absIdx);
switch (type) {
case LUA_TNIL:
return EventValue();
case LUA_TNUMBER: {
lua_Number num = lua_tonumber(L, absIdx);
lua_Integer intVal = lua_tointeger(L, absIdx);
// Check if it's an integer (within precision)
if ((lua_Number)intVal == num) {
return EventValue((int64_t)intVal);
}
return EventValue((double)num);
}
case LUA_TSTRING:
return EventValue(lua_tostring(L, absIdx));
case LUA_TBOOLEAN:
return EventValue((int64_t)(lua_toboolean(L, absIdx) ? 1 : 0));
case LUA_TTABLE: {
// Check if it's an array (all integer keys)
bool isArray = true;
int maxKey = 0;
bool hasStringElements = false;
bool hasNumberElements = false;
bool hasIntegerElements = false;
lua_pushnil(L);
while (lua_next(L, absIdx) != 0) {
if (lua_type(L, -2) == LUA_TNUMBER) {
int k = (int)lua_tointeger(L, -2);
if (k > maxKey)
maxKey = k;
int elemType = lua_type(L, -1);
if (elemType == LUA_TSTRING)
hasStringElements = true;
else if (elemType == LUA_TNUMBER) {
lua_Number num = lua_tonumber(L, -1);
lua_Integer intVal =
lua_tointeger(L, -1);
if ((lua_Number)intVal == num)
hasIntegerElements = true;
else
hasNumberElements = true;
}
} else {
isArray = false;
}
lua_pop(L, 1);
}
if (isArray && maxKey > 0) {
if (hasStringElements) {
std::vector<std::string> arr;
for (int i = 1; i <= maxKey; i++) {
lua_rawgeti(L, absIdx, i);
if (lua_type(L, -1) == LUA_TSTRING)
arr.push_back(
lua_tostring(L, -1));
lua_pop(L, 1);
}
return EventValue(arr);
} else if (hasIntegerElements) {
std::vector<int64_t> arr;
for (int i = 1; i <= maxKey; i++) {
lua_rawgeti(L, absIdx, i);
if (lua_type(L, -1) == LUA_TNUMBER)
arr.push_back(
(int64_t)lua_tointeger(
L, -1));
lua_pop(L, 1);
}
return EventValue(arr);
} else if (hasNumberElements) {
std::vector<double> arr;
for (int i = 1; i <= maxKey; i++) {
lua_rawgeti(L, absIdx, i);
if (lua_type(L, -1) == LUA_TNUMBER)
arr.push_back(
lua_tonumber(L, -1));
lua_pop(L, 1);
}
return EventValue(arr);
}
}
// Not an array or empty - return nil
return EventValue();
}
default:
return EventValue();
}
}
// ---------------------------------------------------------------------------
// Helper: read a Lua table as EventParams
// ---------------------------------------------------------------------------
/**
* @brief Read a Lua table at the given index as EventParams.
*
* Each key-value pair in the table is converted to an EventValue.
* The _types sub-table is NOT read back (types are inferred from values).
*
* @param L Lua state.
* @param idx Stack index of the table.
* @return EventParams populated from the table.
*/
EventParams readEventParams(lua_State *L, int idx)
{
EventParams params;
int absIdx = lua_absindex(L, idx);
if (!lua_istable(L, absIdx))
return params;
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) {
// Skip _types key (metadata)
if (strcmp(key, "_types") == 0) {
lua_pop(L, 1);
continue;
}
params.m_values[key] = readLuaValue(L, -1);
}
}
lua_pop(L, 1);
}
return params;
}
// ---------------------------------------------------------------------------
// 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 an EventParams payload.
*
* Usage:
* ecs.send_event("collision")
* ecs.send_event("collision", { entity_id = 42, damage = 10 })
* ecs.send_event("collision", { targets = {100, 101, 102} })
*/
static int luaSendEvent(lua_State *L)
{
const char *eventName = luaL_checkstring(L, 1);
EventParams params;
if (lua_gettop(L) >= 2 && lua_istable(L, 2)) {
params = readEventParams(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.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 EventParams &params) {
// 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
pushEventParams(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

View File

@@ -0,0 +1,65 @@
#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 EventParams, which
* supports entity IDs, integers, floats, doubles, strings, and
* arrays of each type.
*
* 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 EventParams fields. Each value is typed:
* params.entity_id -> integer (entity ID)
* params.int_val -> integer
* params.float_val -> number (float)
* params.double_val -> number (double)
* params.string_val -> string
* params.entity_ids -> table {integer, ...} (array of entity IDs)
* params.int_array -> table {integer, ...}
* params.float_array -> table {number, ...}
* params.double_array -> table {number, ...}
* params.string_array -> table {string, ...}
*
* Type metadata is available via params._types table:
* params._types.key = "entity_id" | "int" | "float" | "double" |
* "string" | "entity_id_array" | "int_array" |
* "float_array" | "double_array" | "string_array"
*/
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

View File

@@ -0,0 +1,171 @@
#include "LuaGameModeApi.hpp"
#include "../GameMode.hpp"
#include <OgreLogManager.h>
#include <cstdio>
#include <cstdlib>
namespace editScene
{
// ---------------------------------------------------------------------------
// Lua: ecs.is_editor_mode() -> bool
// ---------------------------------------------------------------------------
static int luaIsEditorMode(lua_State *L)
{
(void)L;
lua_pushboolean(L, isEditorMode() ? 1 : 0);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.is_game_mode() -> bool
// ---------------------------------------------------------------------------
static int luaIsGameMode(lua_State *L)
{
(void)L;
lua_pushboolean(L, isGameMode() ? 1 : 0);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.is_game_playing() -> bool
// ---------------------------------------------------------------------------
static int luaIsGamePlaying(lua_State *L)
{
(void)L;
lua_pushboolean(L, isGamePlaying() ? 1 : 0);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.is_game_menu() -> bool
// ---------------------------------------------------------------------------
static int luaIsGameMenu(lua_State *L)
{
(void)L;
lua_pushboolean(L, isGameMenu() ? 1 : 0);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.is_game_paused() -> bool
// ---------------------------------------------------------------------------
static int luaIsGamePaused(lua_State *L)
{
(void)L;
lua_pushboolean(L, isGamePaused() ? 1 : 0);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.get_game_mode() -> string ("editor" or "game")
// ---------------------------------------------------------------------------
static int luaGetGameMode(lua_State *L)
{
(void)L;
switch (getGameMode()) {
case GameMode::Editor:
lua_pushstring(L, "editor");
break;
case GameMode::Game:
lua_pushstring(L, "game");
break;
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.get_game_play_state() -> string ("menu", "playing", or "paused")
// ---------------------------------------------------------------------------
static int luaGetGamePlayState(lua_State *L)
{
(void)L;
switch (getGamePlayState()) {
case GamePlayState::Menu:
lua_pushstring(L, "menu");
break;
case GamePlayState::Playing:
lua_pushstring(L, "playing");
break;
case GamePlayState::Paused:
lua_pushstring(L, "paused");
break;
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.debug_crash("message") -> crashes the application
// ---------------------------------------------------------------------------
static int luaDebugCrash(lua_State *L)
{
const char *msg = luaL_optstring(L, 1, "debug_crash called");
// Log the message
Ogre::LogManager::getSingleton().logMessage("Lua debug_crash: " +
Ogre::String(msg));
// Print to stderr as well
std::fprintf(stderr, "Lua debug_crash: %s\n", msg);
std::fflush(stderr);
// Trigger a deliberate crash
// Use abort() which is cross-platform and raises SIGABRT
std::abort();
return 0;
}
// ---------------------------------------------------------------------------
// Register all game-mode API functions
// ---------------------------------------------------------------------------
void registerLuaGameModeApi(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);
}
// Predicates
lua_pushcfunction(L, luaIsEditorMode);
lua_setfield(L, -2, "is_editor_mode");
lua_pushcfunction(L, luaIsGameMode);
lua_setfield(L, -2, "is_game_mode");
lua_pushcfunction(L, luaIsGamePlaying);
lua_setfield(L, -2, "is_game_playing");
lua_pushcfunction(L, luaIsGameMenu);
lua_setfield(L, -2, "is_game_menu");
lua_pushcfunction(L, luaIsGamePaused);
lua_setfield(L, -2, "is_game_paused");
// Query functions
lua_pushcfunction(L, luaGetGameMode);
lua_setfield(L, -2, "get_game_mode");
lua_pushcfunction(L, luaGetGamePlayState);
lua_setfield(L, -2, "get_game_play_state");
// Debug crash function
lua_pushcfunction(L, luaDebugCrash);
lua_setfield(L, -2, "debug_crash");
// Set the global
lua_setglobal(L, "ecs");
}
} // namespace editScene

View File

@@ -0,0 +1,39 @@
#ifndef EDITSCENE_LUA_GAMEMODE_API_HPP
#define EDITSCENE_LUA_GAMEMODE_API_HPP
#pragma once
#include <lua.hpp>
/**
* @file LuaGameModeApi.hpp
* @brief Lua API for querying editor/game mode state.
*
* Provides functions to check whether the application is in editor mode
* or game mode, and what the current gameplay state is.
*
* Exposed Lua globals (in the "ecs" table):
* ecs.is_editor_mode() -> bool
* ecs.is_game_mode() -> bool
* ecs.is_game_playing() -> bool
* ecs.is_game_menu() -> bool
* ecs.is_game_paused() -> bool
* ecs.get_game_mode() -> string ("editor" or "game")
* ecs.get_game_play_state() -> string ("menu", "playing", or "paused")
* ecs.debug_crash(msg) -> prints msg to log/stderr, then calls std::abort()
*/
namespace editScene
{
/**
* @brief Register all game-mode Lua API functions into the "ecs" global table.
*
* Adds mode query functions to the existing "ecs" table.
*
* @param L The Lua state.
*/
void registerLuaGameModeApi(lua_State *L);
} // namespace editScene
#endif // EDITSCENE_LUA_GAMEMODE_API_HPP

View 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

View 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

View File

@@ -6,17 +6,46 @@ int main(int argc, char *argv[])
{
try {
EditorApp app;
app.initApp();
// Auto-load scene if provided as argument
if (argc > 1) {
std::cout << "Auto-loading scene: " << argv[1] << std::endl;
SceneSerializer serializer(*app.getWorld(), app.getSceneManager());
if (!serializer.loadFromFile(argv[1])) {
std::cerr << "Failed to load scene: " << serializer.getLastError() << std::endl;
// Parse command line arguments
bool gameMode = false;
bool debugBuoyancy = false;
Ogre::String sceneFile;
for (int i = 1; i < argc; i++) {
Ogre::String arg = argv[i];
if (arg == "--game") {
gameMode = true;
} else if (arg == "--debug-buoyancy") {
debugBuoyancy = true;
} else if (arg.length() > 0 && arg[0] != '-') {
sceneFile = arg;
}
}
if (gameMode) {
app.setGameMode(EditorApp::GameMode::Game);
}
if (debugBuoyancy) {
app.setDebugBuoyancy(true);
}
app.initApp();
// Auto-load scene if provided as argument (editor mode only)
if (!sceneFile.empty() &&
app.getGameMode() == EditorApp::GameMode::Editor) {
std::cout << "Auto-loading scene: " << sceneFile
<< std::endl;
SceneSerializer serializer(*app.getWorld(),
app.getSceneManager());
if (!serializer.loadFromFile(sceneFile, nullptr)) {
std::cerr << "Failed to load scene: "
<< serializer.getLastError()
<< std::endl;
}
}
app.getRoot()->startRendering();
app.closeApp();
} catch (const std::exception &e) {

View File

@@ -90,10 +90,10 @@ public:
Layers::MOVING; // Non moving only collides with moving
case Layers::MOVING:
return true; // Moving collides with everything
case Layers::SENSORS:
return inObject2 ==
Layers::MOVING; // Non moving only collides with moving
default:
case Layers::SENSORS:
return inObject2 ==
Layers::MOVING; // Non moving only collides with moving
default:
JPH_ASSERT(false);
return false;
}
@@ -110,8 +110,8 @@ public:
mObjectToBroadPhase[Layers::NON_MOVING] =
BroadPhaseLayers::NON_MOVING;
mObjectToBroadPhase[Layers::MOVING] = BroadPhaseLayers::MOVING;
mObjectToBroadPhase[Layers::SENSORS] = BroadPhaseLayers::MOVING;
}
mObjectToBroadPhase[Layers::SENSORS] = BroadPhaseLayers::MOVING;
}
virtual uint GetNumBroadPhaseLayers() const override
{
@@ -269,13 +269,13 @@ public:
void DrawLine(JPH::RVec3Arg inFrom, JPH::RVec3Arg inTo,
JPH::ColorArg inColor) override
{
JPH::Vec4 color = inColor.ToVec4();
mLines.push_back(
{ { (float)inFrom[0], (float)inFrom[1],
(float)inFrom[2] },
{ (float)inTo[0], (float)inTo[1], (float)inTo[2] },
Ogre::ColourValue(color[0], color[1], color[2],
color[3]) });
JPH::Vec4 color = inColor.ToVec4();
mLines.push_back(
{ { (float)inFrom[0], (float)inFrom[1],
(float)inFrom[2] },
{ (float)inTo[0], (float)inTo[1], (float)inTo[2] },
Ogre::ColourValue(color[0], color[1], color[2],
color[3]) });
}
void DrawTriangle(JPH::RVec3Arg inV1, JPH::RVec3Arg inV2,
JPH::RVec3Arg inV3, JPH::ColorArg inColor,
@@ -422,14 +422,14 @@ DebugRenderer::DebugRenderer(Ogre::SceneManager *scnMgr,
pass->setCullingMode(Ogre::CullingMode::CULL_NONE);
pass->setVertexColourTracking(Ogre::TVC_AMBIENT);
pass->setLightingEnabled(false);
pass->setDepthWriteEnabled(false);
pass->setDepthCheckEnabled(false);
pass->setDepthWriteEnabled(false);
pass->setDepthCheckEnabled(false);
DebugRenderer::Initialize();
scnMgr->getRootSceneNode()->attachObject(mObject);
mLines.reserve(6000);
mObject->estimateVertexCount(64000);
mObject->estimateIndexCount(8000);
mObject->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
mObject->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
}
DebugRenderer::~DebugRenderer()
{
@@ -446,6 +446,10 @@ Ogre::Vector3 convert(const JPH::Vec3Arg &vec)
{
return { vec[0], vec[1], vec[2] };
}
Ogre::Vector3 convert(const JPH::RVec3Arg &vec)
{
return { (float)vec[0], (float)vec[1], (float)vec[2] };
}
JPH::RVec3 convert(const Ogre::Vector3 &vec)
{
return { vec.x, vec.y, vec.z };
@@ -462,7 +466,7 @@ void CompoundShapeBuilder::addShape(JPH::ShapeRefC shape,
const Ogre::Vector3 &position,
const Ogre::Quaternion &rotation)
{
shapeSettings.AddShape(JoltPhysics::convert<JPH::Vec3>(position),
shapeSettings.AddShape(JoltPhysics::convert<JPH::Vec3>(position),
JoltPhysics::convert(rotation), shape.GetPtr());
}
JPH::ShapeRefC CompoundShapeBuilder::build()
@@ -556,6 +560,7 @@ class Physics {
std::set<JPH::Character *> characters;
std::set<JPH::BodyID> characterBodies;
bool debugDraw;
JPH::Vec3 gravity = JPH::Vec3(0.0f, -9.8f, 0.0f);
public:
class ActivationListener : public JPH::BodyActivationListener {
@@ -572,13 +577,13 @@ public:
, job_system(JPH::cMaxPhysicsJobs, JPH::cMaxPhysicsBarriers,
std::thread::hardware_concurrency() - 1)
, mDebugRenderer(new DebugRenderer(scnMgr, cameraNode))
, object_vs_broadphase_layer_filter{}
, object_vs_object_layer_filter{}
, object_vs_broadphase_layer_filter{}
, object_vs_object_layer_filter{}
, debugDraw(false)
{
static int instanceCount = 0;
OgreAssert(instanceCount == 0, "Bad initialisation");
instanceCount++;
static int instanceCount = 0;
OgreAssert(instanceCount == 0, "Bad initialisation");
instanceCount++;
// This is the max amount of rigid bodies that you can add to the physics system. If you try to add more you'll get an error.
// Note: This value is low because this is a simple test. For a real project use something in the order of 65536.
@@ -716,7 +721,7 @@ public:
body_interface.RemoveBody(floor->GetID());
body_interface.DestroyBody(floor->GetID());
#endif
physics_system.SetGravity(JPH::Vec3(0, -0.1f, 0));
physics_system.SetGravity(gravity);
}
~Physics()
{
@@ -763,12 +768,12 @@ public:
if (debugDraw)
cCollisionSteps = 4;
while (timeAccumulator >= fixedDeltaTime) {
physics_system.Update(dt, cCollisionSteps,
physics_system.Update(fixedDeltaTime, cCollisionSteps,
&temp_allocator, &job_system);
timeAccumulator -= fixedDeltaTime;
}
for (JPH::BodyID bID : bodies) {
JPH::RVec3 p;
JPH::RVec3 p;
JPH::Quat q;
if (id2node.find(bID) == id2node.end())
continue;
@@ -785,8 +790,15 @@ 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[bID];
if (node)
node->_setDerivedPosition(
JoltPhysics::convert(
ch->GetPosition()));
}
}
if (debugDraw)
@@ -807,9 +819,18 @@ public:
{
debugDraw = enable;
}
JPH::Vec3 getGravity() const
{
return gravity;
}
void setGravity(const JPH::Vec3 &g)
{
gravity = g;
physics_system.SetGravity(gravity);
}
static JPH::ShapeRefC createBoxShape(float x, float y, float z)
{
return new JPH::BoxShape(JPH::Vec3(x, y, z));
return new JPH::BoxShape(JPH::Vec3(x, y, z));
}
static JPH::ShapeRefC createCylinderShape(float halfHeight,
float radius)
@@ -817,7 +838,7 @@ public:
return new JPH::CylinderShape(halfHeight, radius);
}
JPH::BodyCreationSettings createBodyCreationSettings(
JPH::Shape *shape, JPH::RVec3 &position, JPH::Quat &rotation,
JPH::Shape *shape, JPH::RVec3 &position, JPH::Quat &rotation,
JPH::EMotionType motionType, JPH::ObjectLayer layer)
{
JPH::BodyCreationSettings body_settings(
@@ -826,7 +847,7 @@ public:
}
JPH::BodyCreationSettings
createBodyCreationSettings(JPH::ShapeSettings *shapeSettings,
JPH::RVec3 &position, JPH::Quat &rotation,
JPH::RVec3 &position, JPH::Quat &rotation,
JPH::EMotionType motionType,
JPH::ObjectLayer layer)
{
@@ -851,8 +872,8 @@ public:
{
JPH::BodyInterface &body_interface =
physics_system.GetBodyInterface();
body_interface.AddAngularImpulse(
id, JoltPhysics::convert<JPH::Vec3>(impulse));
body_interface.AddAngularImpulse(
id, JoltPhysics::convert<JPH::Vec3>(impulse));
}
JPH::BodyID createBody(const JPH::BodyCreationSettings &settings,
ActivationListener *listener = nullptr)
@@ -898,14 +919,14 @@ public:
msp.ScaleToMass(mass);
bodySettings.mMassPropertiesOverride = msp;
}
JPH::BodyID id = createBody(bodySettings, listener);
if (shape->GetType() == JPH::EShapeType::HeightField) {
JPH::BodyInterface &body_interface =
physics_system.GetBodyInterface();
body_interface.SetFriction(id, 1.0f);
}
return id;
}
JPH::BodyID id = createBody(bodySettings, listener);
if (shape->GetType() == JPH::EShapeType::HeightField) {
JPH::BodyInterface &body_interface =
physics_system.GetBodyInterface();
body_interface.SetFriction(id, 1.0f);
}
return id;
}
JPH::BodyID createBody(const JPH::Shape *shape, float mass,
Ogre::SceneNode *node, JPH::EMotionType motion,
JPH::ObjectLayer layer,
@@ -915,8 +936,8 @@ public:
const Ogre::Quaternion &rotation =
node->_getDerivedOrientation();
std::cout << "body position: " << position << std::endl;
JPH::BodyID id = createBody(shape, mass, position, rotation,
motion, layer, listener);
JPH::BodyID id = createBody(shape, mass, position, rotation,
motion, layer, listener);
id2node[id] = node;
node2id[node] = id;
return id;
@@ -963,7 +984,7 @@ public:
JPH::MutableCompoundShape *master =
static_cast<JPH::MutableCompoundShape *>(
compoundShape.GetPtr());
master->AddShape(JoltPhysics::convert<JPH::Vec3>(position),
master->AddShape(JoltPhysics::convert<JPH::Vec3>(position),
JoltPhysics::convert(rotation),
childShape.GetPtr());
}
@@ -1002,6 +1023,26 @@ public:
characters.insert(ch);
return ch;
}
JPH::CharacterBase *createCharacter(Ogre::SceneNode *node,
JPH::ShapeRefC shape)
{
JPH::CharacterSettings settings;
settings.mLayer = Layers::MOVING;
settings.mShape = shape;
settings.mSupportingVolume =
JPH::Plane(JPH::Vec3::sAxisY(), -0.2f);
JPH::Character *ch = new JPH::Character(
&settings,
JoltPhysics::convert(node->_getDerivedPosition()),
JoltPhysics::convert(node->_getDerivedOrientation()), 0,
&physics_system);
JPH::BodyID id = ch->GetBodyID();
id2node[id] = node;
node2id[node] = id;
characterBodies.insert(id);
characters.insert(ch);
return ch;
}
JPH::ShapeRefC createBoxShape(Ogre::Vector3 extents)
{
JPH::Vec3 h(extents.x, extents.y, extents.z);
@@ -1117,46 +1158,54 @@ public:
for (j = 0; j < indices.size() / 3; j++)
triangles[j] = { indices[j * 3 + 0], indices[j * 3 + 1],
indices[j * 3 + 2] };
// Validate we have enough data
if (vertices.size() < 3) {
Ogre::LogManager::getSingleton().logMessage(
"ERROR: Mesh has too few vertices: " + mesh->getName() +
" (" + Ogre::StringConverter::toString(vertices.size()) + ")");
"ERROR: Mesh has too few vertices: " +
mesh->getName() + " (" +
Ogre::StringConverter::toString(
vertices.size()) +
")");
return JPH::ShapeRefC();
}
if (triangles.size() < 1) {
Ogre::LogManager::getSingleton().logMessage(
"ERROR: Mesh has no triangles: " + mesh->getName());
"ERROR: Mesh has no triangles: " +
mesh->getName());
return JPH::ShapeRefC();
}
JPH::MeshShapeSettings mesh_shape_settings(vertices, triangles);
// Configure settings to be more permissive
mesh_shape_settings.mPerTriangleUserData = false;
// Log mesh stats for debugging
Ogre::LogManager::getSingleton().logMessage(
"Creating mesh shape: " + mesh->getName() +
" - Vertices: " + Ogre::StringConverter::toString(vertices.size()) +
" - Triangles: " + Ogre::StringConverter::toString(triangles.size()));
"Creating mesh shape: " + mesh->getName() +
" - Vertices: " +
Ogre::StringConverter::toString(vertices.size()) +
" - Triangles: " +
Ogre::StringConverter::toString(triangles.size()));
// Sanitize the mesh data to help with validation
mesh_shape_settings.Sanitize();
JPH::ShapeSettings::ShapeResult result =
mesh_shape_settings.Create();
if (!result.Get()) {
Ogre::LogManager::getSingleton().logMessage(
"ERROR: Failed to create mesh shape: " + mesh->getName() +
"ERROR: Failed to create mesh shape: " +
mesh->getName() +
" - Error: " + result.GetError().c_str());
return JPH::ShapeRefC();
}
Ogre::LogManager::getSingleton().logMessage(
"Successfully created mesh shape for: " + mesh->getName());
"Successfully created mesh shape for: " +
mesh->getName());
return result.Get();
}
JPH::ShapeRefC createMeshShape(Ogre::String meshName)
@@ -1167,7 +1216,7 @@ public:
new Ogre::DefaultHardwareBufferManager;
p = Ogre::DefaultHardwareBufferManager::getSingletonPtr();
}
Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load(
meshName, "General");
if (mesh.get()) {
@@ -1177,7 +1226,7 @@ public:
}
return createMeshShape(mesh);
}
Ogre::LogManager::getSingleton().logMessage(
"ERROR: Could not load mesh for collider: " + meshName);
return JPH::ShapeRefC();
@@ -1322,9 +1371,9 @@ public:
{
int i;
JPH::HeightFieldShapeSettings heightfieldSettings(
samples, JoltPhysics::convert<JPH::Vec3>(offset),
JoltPhysics::convert<JPH::Vec3>(scale),
(uint32_t)sampleCount);
samples, JoltPhysics::convert<JPH::Vec3>(offset),
JoltPhysics::convert<JPH::Vec3>(scale),
(uint32_t)sampleCount);
for (i = 0; i < sampleCount; i++) {
memcpy(heightfieldSettings.mHeightSamples.data() +
sampleCount * i,
@@ -1347,10 +1396,10 @@ public:
"bad parameters");
JPH::MutableCompoundShapeSettings settings;
for (i = 0; i < shapes.size(); i++)
settings.AddShape(
JoltPhysics::convert<JPH::Vec3>(positions[i]),
JoltPhysics::convert(rotations[i]),
shapes[i].GetPtr());
settings.AddShape(
JoltPhysics::convert<JPH::Vec3>(positions[i]),
JoltPhysics::convert(rotations[i]),
shapes[i].GetPtr());
JPH::ShapeSettings::ShapeResult result = settings.Create();
OgreAssert(result.Get(), "Can not create compound shape");
return result.Get();
@@ -1366,10 +1415,10 @@ public:
"bad parameters");
JPH::StaticCompoundShapeSettings settings;
for (i = 0; i < shapes.size(); i++)
settings.AddShape(
JoltPhysics::convert<JPH::Vec3>(positions[i]),
JoltPhysics::convert(rotations[i]),
shapes[i].GetPtr());
settings.AddShape(
JoltPhysics::convert<JPH::Vec3>(positions[i]),
JoltPhysics::convert(rotations[i]),
shapes[i].GetPtr());
JPH::ShapeSettings::ShapeResult result = settings.Create();
OgreAssert(result.Get(), "Can not create compound shape");
return result.Get();
@@ -1379,24 +1428,24 @@ public:
JPH::ShapeRefC shape)
{
JPH::OffsetCenterOfMassShapeSettings settings(
JoltPhysics::convert<JPH::Vec3>(offset),
shape.GetPtr());
JoltPhysics::convert<JPH::Vec3>(offset),
shape.GetPtr());
JPH::ShapeSettings::ShapeResult result = settings.Create();
OgreAssert(result.Get(), "Can not create com offset shape");
return result.Get();
}
JPH::ShapeRefC
createRotatedTranslatedShape(const Ogre::Vector3 &offset,
const Ogre::Quaternion rotation,
JPH::ShapeRefC shape)
{
return JPH::RotatedTranslatedShapeSettings(
JoltPhysics::convert<JPH::Vec3>(offset),
JoltPhysics::convert(rotation), shape)
.Create()
.Get();
}
void applyBuoyancyImpulse(JPH::BodyID id,
JPH::ShapeRefC
createRotatedTranslatedShape(const Ogre::Vector3 &offset,
const Ogre::Quaternion rotation,
JPH::ShapeRefC shape)
{
return JPH::RotatedTranslatedShapeSettings(
JoltPhysics::convert<JPH::Vec3>(offset),
JoltPhysics::convert(rotation), shape)
.Create()
.Get();
}
void applyBuoyancyImpulse(JPH::BodyID id,
const Ogre::Vector3 &surfacePosition,
const Ogre::Vector3 &surfaceNormal,
float buoyancy, float linearDrag,
@@ -1407,12 +1456,12 @@ public:
JPH::BodyLockWrite lock(physics_system.GetBodyLockInterface(),
id);
JPH::Body &body = lock.GetBody();
body.ApplyBuoyancyImpulse(
JoltPhysics::convert(surfacePosition),
JoltPhysics::convert<JPH::Vec3>(surfaceNormal),
buoyancy, linearDrag, angularDrag,
JoltPhysics::convert<JPH::Vec3>(fluidVelocity),
JoltPhysics::convert<JPH::Vec3>(gravity), dt);
body.ApplyBuoyancyImpulse(
JoltPhysics::convert(surfacePosition),
JoltPhysics::convert<JPH::Vec3>(surfaceNormal),
buoyancy, linearDrag, angularDrag,
JoltPhysics::convert<JPH::Vec3>(fluidVelocity),
JoltPhysics::convert<JPH::Vec3>(gravity), dt);
}
void applyBuoyancyImpulse(JPH::BodyID id,
const Ogre::Vector3 &surfacePosition,
@@ -1424,12 +1473,12 @@ public:
JPH::BodyLockWrite lock(physics_system.GetBodyLockInterface(),
id);
JPH::Body &body = lock.GetBody();
body.ApplyBuoyancyImpulse(
JoltPhysics::convert(surfacePosition),
JoltPhysics::convert<JPH::Vec3>(surfaceNormal),
buoyancy, linearDrag, angularDrag,
JoltPhysics::convert<JPH::Vec3>(fluidVelocity),
physics_system.GetGravity(), dt);
body.ApplyBuoyancyImpulse(
JoltPhysics::convert(surfacePosition),
JoltPhysics::convert<JPH::Vec3>(surfaceNormal),
buoyancy, linearDrag, angularDrag,
JoltPhysics::convert<JPH::Vec3>(fluidVelocity),
physics_system.GetGravity(), dt);
}
bool isActive(JPH::BodyID id)
{
@@ -1452,7 +1501,7 @@ public:
void getPositionAndRotation(JPH::BodyID id, Ogre::Vector3 &position,
Ogre::Quaternion &rotation)
{
JPH::RVec3 _position;
JPH::RVec3 _position;
JPH::Quat _rotation;
physics_system.GetBodyInterface().GetPositionAndRotation(
id, _position, _rotation);
@@ -1507,15 +1556,26 @@ public:
return physics_system.GetBodyInterface().SetFriction(id,
friction);
}
float getGravityFactor(JPH::BodyID id)
{
return physics_system.GetBodyInterface().GetGravityFactor(id);
}
void setGravityFactor(JPH::BodyID id, float factor)
{
return physics_system.GetBodyInterface().SetGravityFactor(
id, factor);
}
void broadphaseQuery(float dt, const Ogre::Vector3 &position,
std::set<JPH::BodyID> &inWater)
{
JPH::RVec3 surface_point = JoltPhysics::convert(
JPH::RVec3 surface_point = JoltPhysics::convert(
position + Ogre::Vector3(0, -0.1f, 0));
MyCollector collector(&physics_system, surface_point,
JPH::Vec3::sAxisY(), dt);
JPH::Vec3::sAxisY(), dt);
// Apply buoyancy to all bodies that intersect with the water
// Detects bodies up to 0.1 units above the surface point
// and up to 1000 units below (deep underwater)
JPH::AABox water_box(-JPH::Vec3(1000, 1000, 1000),
JPH::Vec3(1000, 0.1f, 1000));
water_box.Translate(JPH::Vec3(surface_point));
@@ -1536,36 +1596,50 @@ public:
}
}
bool raycastQuery(Ogre::Vector3 startPoint, Ogre::Vector3 endPoint,
Ogre::Vector3 &position, JPH::BodyID &id)
Ogre::Vector3 &position, JPH::BodyID &id)
{
int i;
Ogre::Vector3 direction = endPoint - startPoint;
JPH::RRayCast ray{ JoltPhysics::convert(startPoint),
JoltPhysics::convert<JPH::Vec3>(direction) };
JoltPhysics::convert<JPH::Vec3>(direction) };
JPH::RayCastResult hit;
bool hadHit = physics_system.GetNarrowPhaseQuery().CastRay(
ray, hit, {},
JPH::SpecifiedObjectLayerFilter(Layers::NON_MOVING));
if (hadHit) {
if (hadHit) {
position = JoltPhysics::convert(
ray.GetPointOnRay(hit.mFraction));
id = hit.mBodyID;
}
id = hit.mBodyID;
}
return hadHit;
}
bool bodyIsCharacter(JPH::BodyID id) const
{
return characterBodies.find(id) != characterBodies.end();
}
void destroyCharacter(std::shared_ptr<JPH::Character> ch)
{
characterBodies.erase(characterBodies.find(ch->GetBodyID()));
characters.erase(ch.get());
Ogre::SceneNode *node = id2node[ch->GetBodyID()];
id2node.erase(ch->GetBodyID());
node2id.erase(node);
ch = nullptr;
}
bool bodyIsCharacter(JPH::BodyID id) const
{
return characterBodies.find(id) != characterBodies.end();
}
void destroyCharacter(std::shared_ptr<JPH::Character> ch)
{
characterBodies.erase(characterBodies.find(ch->GetBodyID()));
characters.erase(ch.get());
Ogre::SceneNode *node = id2node[ch->GetBodyID()];
id2node.erase(ch->GetBodyID());
node2id.erase(node);
ch = nullptr;
}
Ogre::SceneNode *getSceneNodeFromBodyID(JPH::BodyID id) const
{
auto it = id2node.find(id);
if (it != id2node.end())
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()
@@ -1583,20 +1657,20 @@ JoltPhysicsWrapper::JoltPhysicsWrapper(Ogre::SceneManager *scnMgr,
// This needs to be done before any other Jolt function is called.
JPH::RegisterDefaultAllocator();
// Install trace and assert callbacks
// Install trace and assert callbacks
JPH::Trace = TraceImpl;
JPH_IF_ENABLE_ASSERTS(JPH::AssertFailed = AssertFailedImpl;)
// Create a factory, this class is responsible for creating instances of classes based on their name or hash and is mainly used for deserialization of saved data.
// It is not directly used in this example but still required.
// Create a factory, this class is responsible for creating instances of classes based on their name or hash and is mainly used for deserialization of saved data.
// It is not directly used in this example but still required.
JPH::Factory::sInstance = new JPH::Factory();
// Register all physics types with the factory and install their collision handlers with the CollisionDispatch class.
// If you have your own custom shape types you probably need to register their handlers with the CollisionDispatch before calling this function.
// If you implement your own default material (PhysicsMaterial::sDefault) make sure to initialize it before this function or else this function will create one for you.
// Register all physics types with the factory and install their collision handlers with the CollisionDispatch class.
// If you have your own custom shape types you probably need to register their handlers with the CollisionDispatch before calling this function.
// If you implement your own default material (PhysicsMaterial::sDefault) make sure to initialize it before this function or else this function will create one for you.
JPH::RegisterTypes();
phys = std::make_unique<Physics>(scnMgr, cameraNode, nullptr,
&contacts);
phys = std::make_unique<Physics>(scnMgr, cameraNode, nullptr,
&contacts);
}
JoltPhysicsWrapper::~JoltPhysicsWrapper()
@@ -1631,7 +1705,7 @@ JPH::ShapeRefC JoltPhysicsWrapper::createSphereShape(float radius)
}
JPH::ShapeRefC JoltPhysicsWrapper::createCapsuleShape(float halfHeight,
float radius)
float radius)
{
return phys->createCapsuleShape(halfHeight, radius);
}
@@ -1691,14 +1765,14 @@ JPH::ShapeRefC
JoltPhysicsWrapper::createOffsetCenterOfMassShape(const Ogre::Vector3 &offset,
JPH::ShapeRefC shape)
{
return phys->createOffsetCenterOfMassShape(offset, shape);
return phys->createOffsetCenterOfMassShape(offset, shape);
}
JPH::ShapeRefC JoltPhysicsWrapper::createRotatedTranslatedShape(
const Ogre::Vector3 &offset, const Ogre::Quaternion rotation,
JPH::ShapeRefC shape)
const Ogre::Vector3 &offset, const Ogre::Quaternion rotation,
JPH::ShapeRefC shape)
{
return phys->createRotatedTranslatedShape(offset, rotation, shape);
return phys->createRotatedTranslatedShape(offset, rotation, shape);
}
JPH::BodyID
@@ -1742,6 +1816,11 @@ JPH::CharacterBase *JoltPhysicsWrapper::createCharacter(Ogre::SceneNode *node,
{
return phys->createCharacter(node, characterHeight, characterRadius);
}
JPH::CharacterBase *JoltPhysicsWrapper::createCharacter(Ogre::SceneNode *node,
JPH::ShapeRefC shape)
{
return phys->createCharacter(node, shape);
}
void JoltPhysicsWrapper::addShapeToCompound(JPH::Ref<JPH::Shape> compoundShape,
JPH::ShapeRefC childShape,
const Ogre::Vector3 &position,
@@ -1761,6 +1840,14 @@ void JoltPhysicsWrapper::setDebugDraw(bool enable)
{
phys->setDebugDraw(enable);
}
Ogre::Vector3 JoltPhysicsWrapper::getGravity() const
{
return JoltPhysics::convert(phys->getGravity());
}
void JoltPhysicsWrapper::setGravity(const Ogre::Vector3 &gravity)
{
phys->setGravity(JoltPhysics::convert<JPH::Vec3>(gravity));
}
void JoltPhysicsWrapper::broadphaseQuery(float dt,
const Ogre::Vector3 &position,
std::set<JPH::BodyID> &inWater)
@@ -1841,6 +1928,14 @@ void JoltPhysicsWrapper::setFriction(JPH::BodyID id, float friction)
{
phys->setFriction(id, friction);
}
float JoltPhysicsWrapper::getGravityFactor(JPH::BodyID id)
{
return phys->getGravityFactor(id);
}
void JoltPhysicsWrapper::setGravityFactor(JPH::BodyID id, float factor)
{
phys->setGravityFactor(id, factor);
}
void JoltPhysicsWrapper::addAngularImpulse(const JPH::BodyID &id,
const Ogre::Vector3 &impulse)
{
@@ -1867,19 +1962,31 @@ void JoltPhysicsWrapper::removeContactListener(const JPH::BodyID &id)
}
bool JoltPhysicsWrapper::raycastQuery(Ogre::Vector3 startPoint,
Ogre::Vector3 endPoint,
Ogre::Vector3 &position, JPH::BodyID &id)
Ogre::Vector3 &position, JPH::BodyID &id)
{
return phys->raycastQuery(startPoint, endPoint, position, id);
return phys->raycastQuery(startPoint, endPoint, position, id);
}
bool JoltPhysicsWrapper::bodyIsCharacter(JPH::BodyID id) const
{
return phys->bodyIsCharacter(id);
return phys->bodyIsCharacter(id);
}
void JoltPhysicsWrapper::destroyCharacter(std::shared_ptr<JPH::Character> ch)
{
phys->destroyCharacter(ch);
phys->destroyCharacter(ch);
}
Ogre::SceneNode *
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;

Some files were not shown because too many files have changed in this diff Show More