Compare commits
83 Commits
64b03abb48
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 11530dd7fc | |||
| 3fd167ebff | |||
| 5952a96ee6 | |||
| 76c3ead4a8 | |||
| 39a053d4ee | |||
| c5da977857 | |||
| 3e7b0169d5 | |||
| f918c5cefb | |||
| 976ced3731 | |||
| 0fd8deaf53 | |||
| 4d843c18c7 | |||
| 0ed83966da | |||
| 998984f75a | |||
| 02fa78764a | |||
| abe6eef6b3 | |||
| cca732b41b | |||
| 8507a3a501 | |||
| b9cce0248a | |||
| fa49bb5005 | |||
| 37441aa8fd | |||
| a1b74aa2d5 | |||
| c80d9c96e6 | |||
| a75db85027 | |||
| 7563937ab8 | |||
| 425bb8411d | |||
| 9b29b68b33 | |||
| 7557c710fb | |||
| ce2f6c1306 | |||
| e0e8e316d4 | |||
| abd2dc22d3 | |||
| a5df60769f | |||
| 75ba39895f | |||
| 2cff982473 | |||
| 3bd2801d1d | |||
| 2e358275f0 | |||
| 5ed7552164 | |||
| 2b3482da88 | |||
| 1d2c330481 | |||
| a0d2561587 | |||
| e95b904f4e | |||
| 9d4fad1d10 | |||
| 4335a8cb05 | |||
| d55bf970e0 | |||
| 30814ea35a | |||
| 35f50f7f51 | |||
| ca5b5b3052 | |||
| 7e4e8f6638 | |||
| c6fb3bb463 | |||
| 1411990def | |||
| 1488d7d918 | |||
| ef708fa14a | |||
| 6d7fcb1157 | |||
| 4313d190f9 | |||
| a2173114b9 | |||
| fb6881998c | |||
| 529476d8cd | |||
| 43e9fb330f | |||
| a392eb0bf9 | |||
| e2960d67e4 | |||
| 79b6af1fff | |||
| 863c401230 | |||
| eec0d8f6f7 | |||
| c2a1db5a65 | |||
| 77f93659d5 | |||
| febeb8ff8d | |||
| 611dcd0d46 | |||
| e6494936d6 | |||
| e3b90e8bba | |||
| 7846082220 | |||
| a955f0b218 | |||
| da4a1a6722 | |||
| 21879c2784 | |||
| 5377d1a75a | |||
| 03f72bdd77 | |||
| 3c47a87768 | |||
| 4ba28fe512 | |||
| 9f2f0be4a3 | |||
| 7d64ba30cb | |||
| 82c0e8c6ce | |||
| 3798f227a7 | |||
| 19e4d80741 | |||
| d8122e3275 | |||
| 0ebba40867 |
@@ -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()
|
||||
|
||||
@@ -73,8 +73,6 @@ FileSystem=resources/fonts
|
||||
[LuaScripts]
|
||||
FileSystem=lua-scripts
|
||||
|
||||
#[Characters]
|
||||
#FileSystem=./characters
|
||||
[Audio]
|
||||
FileSystem=./audio/gui
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +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
|
||||
@@ -37,14 +86,39 @@ set(EDITSCENE_SOURCES
|
||||
ui/ProceduralMaterialEditor.cpp
|
||||
ui/PrimitiveEditor.cpp
|
||||
ui/TriangleBufferEditor.cpp
|
||||
ui/CharacterSlotsEditor.cpp
|
||||
ui/AnimationTreeEditor.cpp
|
||||
ui/AnimationTreeTemplateEditor.cpp
|
||||
ui/CharacterEditor.cpp
|
||||
ui/CellGridEditor.cpp
|
||||
ui/LotEditor.cpp
|
||||
ui/DistrictEditor.cpp
|
||||
ui/TownEditor.cpp
|
||||
ui/RoofEditor.cpp
|
||||
ui/RoomEditor.cpp
|
||||
|
||||
|
||||
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
|
||||
@@ -53,12 +127,35 @@ set(EDITSCENE_SOURCES
|
||||
components/ProceduralMaterialModule.cpp
|
||||
components/PrimitiveModule.cpp
|
||||
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
|
||||
@@ -69,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
|
||||
@@ -79,15 +181,68 @@ set(EDITSCENE_HEADERS
|
||||
components/ProceduralMaterial.hpp
|
||||
components/Primitive.hpp
|
||||
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
|
||||
@@ -108,16 +263,51 @@ set(EDITSCENE_HEADERS
|
||||
ui/ProceduralMaterialEditor.hpp
|
||||
ui/PrimitiveEditor.hpp
|
||||
ui/TriangleBufferEditor.hpp
|
||||
ui/CharacterSlotsEditor.hpp
|
||||
ui/AnimationTreeEditor.hpp
|
||||
ui/AnimationTreeTemplateEditor.hpp
|
||||
ui/CharacterEditor.hpp
|
||||
ui/CellGridEditor.hpp
|
||||
ui/LotEditor.hpp
|
||||
ui/DistrictEditor.hpp
|
||||
ui/TownEditor.hpp
|
||||
ui/RoofEditor.hpp
|
||||
ui/RoomEditor.hpp
|
||||
|
||||
|
||||
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})
|
||||
@@ -134,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.)
|
||||
@@ -151,8 +485,18 @@ add_custom_command(TARGET editSceneEditor POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_BINARY_DIR}/resources"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/resources"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_BINARY_DIR}/characters"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/characters"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_BINARY_DIR}/lua-scripts"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/lua-scripts"
|
||||
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"
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
||||
#include <OgreRenderTargetListener.h>
|
||||
#include <flecs.h>
|
||||
#include <memory>
|
||||
#include "lua/LuaState.hpp"
|
||||
|
||||
// Forward declarations
|
||||
class EditorUISystem;
|
||||
@@ -21,7 +22,56 @@ class StaticGeometrySystem;
|
||||
class ProceduralTextureSystem;
|
||||
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
|
||||
@@ -30,28 +80,46 @@ class CellGridSystem;
|
||||
class ImGuiRenderListener : public Ogre::RenderTargetListener {
|
||||
public:
|
||||
ImGuiRenderListener(Ogre::ImGuiOverlay *imguiOverlay,
|
||||
EditorUISystem *uiSystem);
|
||||
EditorUISystem *uiSystem,
|
||||
Ogre::RenderWindow *renderWindow,
|
||||
EditorApp *editorApp);
|
||||
|
||||
void preViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override;
|
||||
void postViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override;
|
||||
void
|
||||
preViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override;
|
||||
void
|
||||
postViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override;
|
||||
|
||||
private:
|
||||
Ogre::ImGuiOverlay *m_imguiOverlay;
|
||||
EditorUISystem *m_uiSystem;
|
||||
Ogre::RenderWindow *m_renderWindow;
|
||||
EditorApp *m_editorApp;
|
||||
|
||||
// Timer for delta time calculation
|
||||
Ogre::Timer m_timer;
|
||||
unsigned long m_lastTime = 0;
|
||||
float m_deltaTime = 0.0f;
|
||||
|
||||
// Frame stats (updated in postViewportUpdate)
|
||||
int m_lastBatchCount = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
// OgreBites::ApplicationContext overrides
|
||||
void setup() override;
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &evt) override;
|
||||
void locateResources() override;
|
||||
|
||||
// OgreBites::InputListener overrides
|
||||
bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override;
|
||||
@@ -69,9 +137,75 @@ 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 { return m_sceneMgr; }
|
||||
Ogre::SceneManager *getSceneManager() const
|
||||
{
|
||||
return m_sceneMgr;
|
||||
}
|
||||
flecs::world *getWorld()
|
||||
{
|
||||
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
|
||||
@@ -88,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;
|
||||
@@ -95,10 +233,41 @@ private:
|
||||
std::unique_ptr<ProceduralTextureSystem> m_proceduralTextureSystem;
|
||||
std::unique_ptr<ProceduralMaterialSystem> m_proceduralMaterialSystem;
|
||||
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
|
||||
|
||||
37
src/features/editScene/GameMode.cpp
Normal file
37
src/features/editScene/GameMode.cpp
Normal 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
|
||||
101
src/features/editScene/GameMode.hpp
Normal file
101
src/features/editScene/GameMode.hpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
|
||||
515
src/features/editScene/components/ActionDatabase.cpp
Normal file
515
src/features/editScene/components/ActionDatabase.cpp
Normal 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();
|
||||
});
|
||||
}
|
||||
114
src/features/editScene/components/ActionDatabase.hpp
Normal file
114
src/features/editScene/components/ActionDatabase.hpp
Normal 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
|
||||
47
src/features/editScene/components/ActionDatabaseModule.cpp
Normal file
47
src/features/editScene/components/ActionDatabaseModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
39
src/features/editScene/components/ActionDebug.hpp
Normal file
39
src/features/editScene/components/ActionDebug.hpp
Normal 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
|
||||
21
src/features/editScene/components/ActionDebugModule.cpp
Normal file
21
src/features/editScene/components/ActionDebugModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
44
src/features/editScene/components/Actuator.hpp
Normal file
44
src/features/editScene/components/Actuator.hpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef EDITSCENE_ACTUATOR_HPP
|
||||
#define EDITSCENE_ACTUATOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Actuator component.
|
||||
*
|
||||
* An interactive object visible only to the player character.
|
||||
* When the player is within radius + height range, an on-screen
|
||||
* prompt appears and the action can be triggered with the action key.
|
||||
*
|
||||
* Unlike SmartObject, Actuators do not use pathfinding or path
|
||||
* following — they are instantaneous interactions.
|
||||
*/
|
||||
struct ActuatorComponent {
|
||||
// Interaction radius in XZ plane
|
||||
float radius = 1.5f;
|
||||
|
||||
// Maximum height difference for interaction
|
||||
float height = 1.8f;
|
||||
|
||||
// Names of GOAP actions (from ActionDatabase) that this actuator provides
|
||||
std::vector<Ogre::String> actionNames;
|
||||
|
||||
// Runtime: cooldown timer (seconds remaining)
|
||||
float cooldownTimer = 0.0f;
|
||||
|
||||
// Runtime: currently executing an action
|
||||
bool isExecuting = false;
|
||||
|
||||
ActuatorComponent() = default;
|
||||
|
||||
explicit ActuatorComponent(float radius_, float height_)
|
||||
: radius(radius_)
|
||||
, height(height_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTUATOR_HPP
|
||||
19
src/features/editScene/components/ActuatorModule.cpp
Normal file
19
src/features/editScene/components/ActuatorModule.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "Actuator.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ActuatorEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Actuator", "Game", ActuatorComponent, ActuatorEditor)
|
||||
{
|
||||
registry.registerComponent<ActuatorComponent>(
|
||||
"Actuator", "Game", std::make_unique<ActuatorEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ActuatorComponent>())
|
||||
e.set<ActuatorComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ActuatorComponent>())
|
||||
e.remove<ActuatorComponent>();
|
||||
});
|
||||
}
|
||||
9
src/features/editScene/components/AnimationTree.cpp
Normal file
9
src/features/editScene/components/AnimationTree.cpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#include "AnimationTree.hpp"
|
||||
|
||||
AnimationTreeComponent::AnimationTreeComponent()
|
||||
: root()
|
||||
, enabled(true)
|
||||
, useRootMotion(false)
|
||||
, dirty(true)
|
||||
{
|
||||
}
|
||||
165
src/features/editScene/components/AnimationTree.hpp
Normal file
165
src/features/editScene/components/AnimationTree.hpp
Normal file
@@ -0,0 +1,165 @@
|
||||
#ifndef EDITSCENE_ANIMATIONTREE_HPP
|
||||
#define EDITSCENE_ANIMATIONTREE_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* A node in the animation tree.
|
||||
*
|
||||
* Node types:
|
||||
* "output" - Root output node, optional speed multiplier (1 child)
|
||||
* "stateMachine" - Cross-fades between child states, named for lookup
|
||||
* "state" - A named state within a state machine (1 child)
|
||||
* "speed" - Playback speed multiplier (1 child)
|
||||
* "animation" - Leaf referencing an Ogre animation by name
|
||||
*/
|
||||
struct AnimationTreeNode {
|
||||
Ogre::String type = "animation";
|
||||
Ogre::String name;
|
||||
Ogre::String animationName;
|
||||
float speed = 1.0f;
|
||||
float fadeSpeed = 7.5f;
|
||||
std::vector<AnimationTreeNode> children;
|
||||
/* For stateMachine nodes: auto-transition when animation ends */
|
||||
std::unordered_map<Ogre::String, Ogre::String> endTransitions;
|
||||
|
||||
AnimationTreeNode() = default;
|
||||
|
||||
AnimationTreeNode *findChild(const Ogre::String &childName)
|
||||
{
|
||||
for (auto &child : children) {
|
||||
if (child.name == childName)
|
||||
return &child;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AnimationTreeNode *findChild(
|
||||
const Ogre::String &childName) const
|
||||
{
|
||||
for (const auto &child : children) {
|
||||
if (child.name == childName)
|
||||
return &child;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AnimationTreeNode *findStateMachine(const Ogre::String &smName)
|
||||
{
|
||||
if (type == "stateMachine" && name == smName)
|
||||
return this;
|
||||
for (auto &child : children) {
|
||||
auto *found = child.findStateMachine(smName);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AnimationTreeNode *findStateMachine(
|
||||
const Ogre::String &smName) const
|
||||
{
|
||||
if (type == "stateMachine" && name == smName)
|
||||
return this;
|
||||
for (const auto &child : children) {
|
||||
auto *found = child.findStateMachine(smName);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AnimationTreeNode *findState(const Ogre::String &stateName)
|
||||
{
|
||||
if (type == "state" && name == stateName)
|
||||
return this;
|
||||
for (auto &child : children) {
|
||||
auto *found = child.findState(stateName);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AnimationTreeNode *findState(
|
||||
const Ogre::String &stateName) const
|
||||
{
|
||||
if (type == "state" && name == stateName)
|
||||
return this;
|
||||
for (const auto &child : children) {
|
||||
auto *found = child.findState(stateName);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AnimationTreeNode *findAnimationLeaf()
|
||||
{
|
||||
if (type == "animation")
|
||||
return this;
|
||||
for (auto &child : children) {
|
||||
auto *found = child.findAnimationLeaf();
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AnimationTreeNode *findAnimationLeaf() const
|
||||
{
|
||||
if (type == "animation")
|
||||
return this;
|
||||
for (const auto &child : children) {
|
||||
auto *found = child.findAnimationLeaf();
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void collectStateMachines(std::vector<AnimationTreeNode *> &out)
|
||||
{
|
||||
if (type == "stateMachine")
|
||||
out.push_back(this);
|
||||
for (auto &child : children)
|
||||
child.collectStateMachines(out);
|
||||
}
|
||||
|
||||
void collectStateMachines(
|
||||
std::vector<const AnimationTreeNode *> &out) const
|
||||
{
|
||||
if (type == "stateMachine")
|
||||
out.push_back(this);
|
||||
for (const auto &child : children)
|
||||
child.collectStateMachines(out);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Animation tree component for hierarchical state-machine-based animation.
|
||||
*
|
||||
* The tree is evaluated each frame by AnimationTreeSystem to determine
|
||||
* which Ogre AnimationStates are active and their blend weights.
|
||||
*/
|
||||
struct AnimationTreeComponent {
|
||||
AnimationTreeNode root;
|
||||
bool enabled = true;
|
||||
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;
|
||||
|
||||
AnimationTreeComponent();
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ANIMATIONTREE_HPP
|
||||
57
src/features/editScene/components/AnimationTreeModule.cpp
Normal file
57
src/features/editScene/components/AnimationTreeModule.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "AnimationTree.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/AnimationTreeEditor.hpp"
|
||||
#include "Transform.hpp"
|
||||
|
||||
static AnimationTreeComponent createDefaultTree()
|
||||
{
|
||||
AnimationTreeComponent at;
|
||||
|
||||
at.root.type = "output";
|
||||
at.root.speed = 1.0f;
|
||||
|
||||
AnimationTreeNode sm;
|
||||
sm.type = "stateMachine";
|
||||
sm.name = "main";
|
||||
sm.fadeSpeed = 7.5f;
|
||||
|
||||
AnimationTreeNode state;
|
||||
state.type = "state";
|
||||
state.name = "idle";
|
||||
|
||||
AnimationTreeNode anim;
|
||||
anim.type = "animation";
|
||||
anim.animationName = "idle";
|
||||
|
||||
state.children.push_back(anim);
|
||||
sm.children.push_back(state);
|
||||
at.root.children.push_back(sm);
|
||||
|
||||
return at;
|
||||
}
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering",
|
||||
AnimationTreeComponent, AnimationTreeEditor)
|
||||
{
|
||||
registry.registerComponent<AnimationTreeComponent>(
|
||||
"Animation Tree", "Rendering",
|
||||
std::make_unique<AnimationTreeEditor>(sceneMgr),
|
||||
/* Adder */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<TransformComponent>()) {
|
||||
TransformComponent transform;
|
||||
transform.node =
|
||||
sceneMgr->getRootSceneNode()
|
||||
->createChildSceneNode();
|
||||
e.set<TransformComponent>(transform);
|
||||
}
|
||||
AnimationTreeComponent at = createDefaultTree();
|
||||
at.dirty = true;
|
||||
e.set<AnimationTreeComponent>(at);
|
||||
},
|
||||
/* Remover */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
(void)sceneMgr;
|
||||
e.remove<AnimationTreeComponent>();
|
||||
});
|
||||
}
|
||||
20
src/features/editScene/components/AnimationTreeTemplate.hpp
Normal file
20
src/features/editScene/components/AnimationTreeTemplate.hpp
Normal 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
|
||||
@@ -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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
130
src/features/editScene/components/BehaviorTree.hpp
Normal file
130
src/features/editScene/components/BehaviorTree.hpp
Normal 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
|
||||
46
src/features/editScene/components/BuoyancyInfo.hpp
Normal file
46
src/features/editScene/components/BuoyancyInfo.hpp
Normal 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
|
||||
23
src/features/editScene/components/BuoyancyInfoModule.cpp
Normal file
23
src/features/editScene/components/BuoyancyInfoModule.cpp
Normal 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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -9,7 +9,7 @@ REGISTER_COMPONENT("Camera", CameraComponent, CameraEditor)
|
||||
{
|
||||
registry.registerComponent<CameraComponent>(
|
||||
"Camera",
|
||||
std::make_unique<CameraEditor>(sceneMgr),
|
||||
std::make_unique<CameraEditor>(sceneMgr, renderWindow),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<CameraComponent>()) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <chrono>
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
|
||||
@@ -59,6 +60,16 @@ namespace CellFlags {
|
||||
IntDoorXNeg | IntDoorXPos | IntDoorZPos | IntDoorZNeg;
|
||||
constexpr uint64_t AllWindows = WindowXNeg | WindowXPos | WindowZPos | WindowZNeg |
|
||||
IntWindowXNeg | IntWindowXPos | IntWindowZPos | IntWindowZNeg;
|
||||
|
||||
// Combined masks for corners (walls + doors + windows in each direction)
|
||||
constexpr uint64_t AllXNeg = WallXNeg | DoorXNeg | WindowXNeg;
|
||||
constexpr uint64_t AllXPos = WallXPos | DoorXPos | WindowXPos;
|
||||
constexpr uint64_t AllZPos = WallZPos | DoorZPos | WindowZPos;
|
||||
constexpr uint64_t AllZNeg = WallZNeg | DoorZNeg | WindowZNeg;
|
||||
constexpr uint64_t AllIntXNeg = IntWallXNeg | IntDoorXNeg | IntWindowXNeg;
|
||||
constexpr uint64_t AllIntXPos = IntWallXPos | IntDoorXPos | IntWindowXPos;
|
||||
constexpr uint64_t AllIntZPos = IntWallZPos | IntDoorZPos | IntWindowZPos;
|
||||
constexpr uint64_t AllIntZNeg = IntWallZNeg | IntDoorZNeg | IntWindowZNeg;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +130,24 @@ struct CellGridComponent {
|
||||
// Generation script (Lua or custom format)
|
||||
std::string generationScript;
|
||||
|
||||
// Texture rectangle names for different parts (from ProceduralTexture)
|
||||
std::string floorRectName;
|
||||
std::string ceilingRectName;
|
||||
std::string extWallRectName;
|
||||
std::string intWallRectName;
|
||||
// Frame texture rectangles
|
||||
std::string extDoorFrameRectName;
|
||||
std::string intDoorFrameRectName;
|
||||
std::string extWindowFrameRectName;
|
||||
std::string intWindowFrameRectName;
|
||||
|
||||
// Roof texture rectangles
|
||||
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;
|
||||
@@ -163,22 +192,168 @@ struct CellGridComponent {
|
||||
/**
|
||||
* @brief Room definition within a cell grid
|
||||
*
|
||||
* Rooms are rectangular areas that can be connected by doors
|
||||
* Rooms are rectangular areas that can be connected by doors.
|
||||
* This is the ECS version of the Lua room() function.
|
||||
*/
|
||||
struct RoomComponent {
|
||||
// Room bounds (in cell coordinates, inclusive)
|
||||
// Room bounds (in cell coordinates)
|
||||
// A room from (minX, minZ) to (maxX-1, maxZ-1) inclusive
|
||||
int minX = 0, minY = 0, minZ = 0;
|
||||
int maxX = 0, maxY = 0, maxZ = 0;
|
||||
int maxX = 1, maxY = 1, maxZ = 1; // exclusive max (size = max - min)
|
||||
|
||||
// Room name/tag for furniture placement rules
|
||||
std::string roomType; // e.g., "bedroom", "kitchen", "hallway"
|
||||
std::vector<std::string> tags;
|
||||
|
||||
// Connected room indices
|
||||
std::vector<int> connectedRooms;
|
||||
// Generation flags
|
||||
bool createFloor = true; // Create floor cells
|
||||
bool createCeiling = true; // Create ceiling cells
|
||||
bool createInteriorWalls = true; // Create iwallx-/+, iwallz-/+ around the room
|
||||
bool createWindows = false; // Convert exterior-facing walls to windows
|
||||
bool fillRoomWithFurniture = false; // Automatically place furniture based on tags
|
||||
unsigned int furnitureSeed = 42; // Seed for deterministic furniture placement
|
||||
float furnitureYOffset = 0.05f; // Y offset for all furniture in this room
|
||||
|
||||
// Exit directions (0-3 = Z-, Z+, X-, X+)
|
||||
std::vector<int> exits;
|
||||
// Dirty flag - triggers regeneration of cell grid
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
|
||||
// Helper to get size
|
||||
int getSizeX() const { return maxX - minX; }
|
||||
int getSizeY() const { return maxY - minY; }
|
||||
int getSizeZ() const { return maxZ - minZ; }
|
||||
|
||||
// Check if a cell position is inside this room
|
||||
bool contains(int x, int y, int z) const {
|
||||
return x >= minX && x < maxX &&
|
||||
y >= minY && y < maxY &&
|
||||
z >= minZ && z < maxZ;
|
||||
}
|
||||
|
||||
// Check if a cell is on the room edge (for wall placement)
|
||||
bool isOnEdge(int x, int z) const {
|
||||
return x == minX || x == maxX - 1 || z == minZ || z == maxZ - 1;
|
||||
}
|
||||
|
||||
// Get the side of the room this cell is on (0=Z-, 1=Z+, 2=X-, 3=X+, -1=not on edge)
|
||||
int getEdgeSide(int x, int z) const {
|
||||
if (!isOnEdge(x, z)) return -1;
|
||||
if (z == minZ) return 0; // Z- (north)
|
||||
if (z == maxZ - 1) return 1; // Z+ (south)
|
||||
if (x == minX) return 2; // X- (west)
|
||||
if (x == maxX - 1) return 3; // X+ (east)
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Persistent unique ID for this room (survives save/load)
|
||||
// If empty, will be auto-generated during serialization
|
||||
std::string persistentId;
|
||||
|
||||
// Connected room persistent IDs (bidirectional connections)
|
||||
// Using persistent IDs instead of entity IDs because entity IDs change on save/load
|
||||
std::vector<std::string> connectedRoomIds;
|
||||
|
||||
// Exit doors for outside-facing walls (0=Z-, 1=Z+, 2=X-, 3=X+)
|
||||
// An exit is always a door placed on a wall that faces outside (not connected to another room)
|
||||
bool exits[4] = { false, false, false, false }; // Z-, Z+, X-, X+
|
||||
|
||||
// Generate a persistent ID if one doesn't exist
|
||||
void ensurePersistentId(flecs::entity_t entityId = 0) {
|
||||
if (persistentId.empty()) {
|
||||
// Generate unique ID based on timestamp + random + optional entity ID
|
||||
auto now = std::chrono::high_resolution_clock::now();
|
||||
auto nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
now.time_since_epoch()).count();
|
||||
persistentId = "room_" + std::to_string(nanos);
|
||||
if (entityId != 0) {
|
||||
persistentId += "_" + std::to_string(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a connection to another room (bidirectional, by persistent ID)
|
||||
void addConnection(const std::string& otherRoomId) {
|
||||
// Check if not already connected
|
||||
for (const auto& id : connectedRoomIds) {
|
||||
if (id == otherRoomId) return;
|
||||
}
|
||||
connectedRoomIds.push_back(otherRoomId);
|
||||
}
|
||||
|
||||
// Remove a connection to another room
|
||||
void removeConnection(const std::string& otherRoomId) {
|
||||
connectedRoomIds.erase(
|
||||
std::remove(connectedRoomIds.begin(), connectedRoomIds.end(), otherRoomId),
|
||||
connectedRoomIds.end()
|
||||
);
|
||||
}
|
||||
|
||||
// Check if connected to a room (by persistent ID)
|
||||
bool isConnectedTo(const std::string& otherRoomId) const {
|
||||
for (const auto& id : connectedRoomIds) {
|
||||
if (id == otherRoomId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper to set all exits at once
|
||||
void setAllExits(bool value) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
exits[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to set a specific exit
|
||||
void setExit(int side, bool value) {
|
||||
if (side >= 0 && side < 4) {
|
||||
exits[side] = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Room exits - defines external exits from a room
|
||||
*
|
||||
* This replaces the Lua create_exit0/1/2/3() functions.
|
||||
* Creates external walls/doors/windows on the room edges that face outside.
|
||||
*/
|
||||
/**
|
||||
* @brief Clear Area - clears cells in a region before room generation
|
||||
*
|
||||
* This replaces the Lua clear_area() and clear_furniture_area() functions.
|
||||
* It clears all cells within the specified bounds before any room generation happens.
|
||||
* This component is processed first in the RoomLayoutSystem.
|
||||
*/
|
||||
struct ClearAreaComponent {
|
||||
// Area bounds (in cell coordinates, inclusive min, exclusive max)
|
||||
int minX = 0, minY = 0, minZ = 0;
|
||||
int maxX = 1, maxY = 1, maxZ = 1;
|
||||
|
||||
// What to clear
|
||||
bool clearCells = true; // Clear cell flags (walls, floors, etc.)
|
||||
bool clearFurniture = true; // Clear furniture placements
|
||||
bool clearRoofs = false; // Clear roof definitions (children with RoofComponent)
|
||||
bool clearRooms = false; // Remove existing room entities (children with RoomComponent)
|
||||
|
||||
// Processed flag - cleared each time dirty is set
|
||||
bool processed = false;
|
||||
|
||||
// Dirty flag - clear again when true
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
|
||||
// Helper to set bounds
|
||||
void setBounds(int x1, int z1, int x2, int z2, int y = 0) {
|
||||
minX = x1; minZ = z1; minY = y;
|
||||
maxX = x2; maxZ = z2; maxY = y + 1;
|
||||
}
|
||||
|
||||
// Helper to get size
|
||||
int getSizeX() const { return maxX - minX; }
|
||||
int getSizeY() const { return maxY - minY; }
|
||||
int getSizeZ() const { return maxZ - minZ; }
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -207,6 +382,11 @@ struct RoofComponent {
|
||||
// Height parameters
|
||||
float baseHeight = 0.5f;
|
||||
float maxHeight = 0.5f;
|
||||
|
||||
// Dirty flag - triggers rebuild when changed
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -242,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;
|
||||
|
||||
@@ -274,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;
|
||||
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
#include "../ui/TownEditor.hpp"
|
||||
#include "../ui/RoofEditor.hpp"
|
||||
#include "../ui/RoomEditor.hpp"
|
||||
|
||||
#include "../ui/ClearAreaEditor.hpp"
|
||||
#include "../ui/FurnitureTemplateEditor.hpp"
|
||||
|
||||
// Register CellGrid component
|
||||
REGISTER_COMPONENT("Cell Grid", CellGridComponent, CellGridEditor)
|
||||
REGISTER_COMPONENT_GROUP("Cell Grid", "Cell Grid", CellGridComponent, CellGridEditor)
|
||||
{
|
||||
registry.registerComponent<CellGridComponent>(
|
||||
"Cell Grid",
|
||||
"Cell Grid", "Cell Grid",
|
||||
std::make_unique<CellGridEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<CellGridComponent>()) {
|
||||
@@ -28,10 +30,10 @@ REGISTER_COMPONENT("Cell Grid", CellGridComponent, CellGridEditor)
|
||||
}
|
||||
|
||||
// Register Lot component
|
||||
REGISTER_COMPONENT("Lot", LotComponent, LotEditor)
|
||||
REGISTER_COMPONENT_GROUP("Lot", "Cell Grid", LotComponent, LotEditor)
|
||||
{
|
||||
registry.registerComponent<LotComponent>(
|
||||
"Lot",
|
||||
"Lot", "Cell Grid",
|
||||
std::make_unique<LotEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<LotComponent>()) {
|
||||
@@ -47,10 +49,10 @@ REGISTER_COMPONENT("Lot", LotComponent, LotEditor)
|
||||
}
|
||||
|
||||
// Register District component
|
||||
REGISTER_COMPONENT("District", DistrictComponent, DistrictEditor)
|
||||
REGISTER_COMPONENT_GROUP("District", "Cell Grid", DistrictComponent, DistrictEditor)
|
||||
{
|
||||
registry.registerComponent<DistrictComponent>(
|
||||
"District",
|
||||
"District", "Cell Grid",
|
||||
std::make_unique<DistrictEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<DistrictComponent>()) {
|
||||
@@ -66,10 +68,10 @@ REGISTER_COMPONENT("District", DistrictComponent, DistrictEditor)
|
||||
}
|
||||
|
||||
// Register Town component
|
||||
REGISTER_COMPONENT("Town", TownComponent, TownEditor)
|
||||
REGISTER_COMPONENT_GROUP("Town", "Cell Grid", TownComponent, TownEditor)
|
||||
{
|
||||
registry.registerComponent<TownComponent>(
|
||||
"Town",
|
||||
"Town", "Cell Grid",
|
||||
std::make_unique<TownEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<TownComponent>()) {
|
||||
@@ -85,10 +87,10 @@ REGISTER_COMPONENT("Town", TownComponent, TownEditor)
|
||||
}
|
||||
|
||||
// Register Roof component
|
||||
REGISTER_COMPONENT("Roof", RoofComponent, RoofEditor)
|
||||
REGISTER_COMPONENT_GROUP("Roof", "Cell Grid", RoofComponent, RoofEditor)
|
||||
{
|
||||
registry.registerComponent<RoofComponent>(
|
||||
"Roof",
|
||||
"Roof", "Cell Grid",
|
||||
std::make_unique<RoofEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<RoofComponent>()) {
|
||||
@@ -103,11 +105,11 @@ REGISTER_COMPONENT("Roof", RoofComponent, RoofEditor)
|
||||
);
|
||||
}
|
||||
|
||||
// Register Room component
|
||||
REGISTER_COMPONENT("Room", RoomComponent, RoomEditor)
|
||||
// Register Room component (in Room Layout group)
|
||||
REGISTER_COMPONENT_GROUP("Room", "Room Layout", RoomComponent, RoomEditor)
|
||||
{
|
||||
registry.registerComponent<RoomComponent>(
|
||||
"Room",
|
||||
"Room", "Room Layout",
|
||||
std::make_unique<RoomEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<RoomComponent>()) {
|
||||
@@ -123,10 +125,10 @@ REGISTER_COMPONENT("Room", RoomComponent, RoomEditor)
|
||||
}
|
||||
|
||||
// Register FurnitureTemplate component
|
||||
REGISTER_COMPONENT("Furniture Template", FurnitureTemplateComponent, FurnitureTemplateEditor)
|
||||
REGISTER_COMPONENT_GROUP("Furniture Template", "Cell Grid", FurnitureTemplateComponent, FurnitureTemplateEditor)
|
||||
{
|
||||
registry.registerComponent<FurnitureTemplateComponent>(
|
||||
"Furniture Template",
|
||||
"Furniture Template", "Cell Grid",
|
||||
std::make_unique<FurnitureTemplateEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<FurnitureTemplateComponent>()) {
|
||||
@@ -140,3 +142,26 @@ REGISTER_COMPONENT("Furniture Template", FurnitureTemplateComponent, FurnitureTe
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register ClearArea component (in Room Layout group)
|
||||
REGISTER_COMPONENT_GROUP("Clear Area", "Room Layout", ClearAreaComponent, ClearAreaEditor)
|
||||
{
|
||||
registry.registerComponent<ClearAreaComponent>(
|
||||
"Clear Area", "Room Layout",
|
||||
std::make_unique<ClearAreaEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<ClearAreaComponent>()) {
|
||||
e.set<ClearAreaComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<ClearAreaComponent>()) {
|
||||
e.remove<ClearAreaComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Note: ExteriorGenerationComponent has been removed.
|
||||
// Exterior generation now automatically runs at the end of the RoomLayoutSystem pipeline.
|
||||
// This mirrors the original Lua behavior where create_exterior() was called after all rooms were defined.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "CellGridModule.hpp"
|
||||
#include "CellGrid.hpp"
|
||||
#include "GeneratedPhysicsTag.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <Ogre.h>
|
||||
|
||||
@@ -41,6 +42,7 @@ void registerComponents(flecs::world& world)
|
||||
|
||||
// FurnitureTemplateComponent
|
||||
world.component<FurnitureTemplateComponent>();
|
||||
world.component<GeneratedPhysicsTag>();
|
||||
}
|
||||
|
||||
} // namespace CellGridModule
|
||||
|
||||
61
src/features/editScene/components/Character.hpp
Normal file
61
src/features/editScene/components/Character.hpp
Normal 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
|
||||
20
src/features/editScene/components/CharacterModule.cpp
Normal file
20
src/features/editScene/components/CharacterModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
30
src/features/editScene/components/CharacterSlots.hpp
Normal file
30
src/features/editScene/components/CharacterSlots.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef EDITSCENE_CHARACTERSLOTS_HPP
|
||||
#define EDITSCENE_CHARACTERSLOTS_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* Multi-slot mesh component for character parts sharing a skeleton.
|
||||
* The "face" slot (or first available slot) serves as the master skeleton.
|
||||
*/
|
||||
struct CharacterSlotsComponent {
|
||||
Ogre::String age = "adult";
|
||||
Ogre::String sex = "male";
|
||||
std::unordered_map<Ogre::String, Ogre::String> slots;
|
||||
bool dirty = true;
|
||||
|
||||
/* 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;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTERSLOTS_HPP
|
||||
35
src/features/editScene/components/CharacterSlotsModule.cpp
Normal file
35
src/features/editScene/components/CharacterSlotsModule.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "CharacterSlots.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/CharacterSlotsEditor.hpp"
|
||||
#include "../systems/CharacterSlotSystem.hpp"
|
||||
#include "Transform.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Character Slots", "Rendering",
|
||||
CharacterSlotsComponent, CharacterSlotsEditor)
|
||||
{
|
||||
CharacterSlotSystem::loadCatalog();
|
||||
|
||||
registry.registerComponent<CharacterSlotsComponent>(
|
||||
"Character Slots",
|
||||
"Rendering",
|
||||
std::make_unique<CharacterSlotsEditor>(sceneMgr),
|
||||
/* Adder */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<TransformComponent>()) {
|
||||
TransformComponent transform;
|
||||
transform.node =
|
||||
sceneMgr->getRootSceneNode()
|
||||
->createChildSceneNode();
|
||||
e.set<TransformComponent>(transform);
|
||||
}
|
||||
CharacterSlotsComponent cs;
|
||||
cs.dirty = true;
|
||||
e.set<CharacterSlotsComponent>(cs);
|
||||
},
|
||||
/* Remover */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
(void)sceneMgr;
|
||||
e.remove<CharacterSlotsComponent>();
|
||||
}
|
||||
);
|
||||
}
|
||||
131
src/features/editScene/components/DialogueComponent.hpp
Normal file
131
src/features/editScene/components/DialogueComponent.hpp
Normal file
@@ -0,0 +1,131 @@
|
||||
#ifndef EDITSCENE_DIALOGUE_COMPONENT_HPP
|
||||
#define EDITSCENE_DIALOGUE_COMPONENT_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Visual-novel style dialogue box component.
|
||||
*
|
||||
* Displays a narration text box at the bottom of the screen with optional
|
||||
* player choices. The dialogue can be driven via the EventBus system
|
||||
* (using "dialogue_show" event) or directly via the component API.
|
||||
*
|
||||
* Only active in game mode (GamePlayState::Playing).
|
||||
*
|
||||
* Event payload (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
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/DialogueEditor.hpp"
|
||||
#include "DialogueComponent.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Dialogue Box", "Game", DialogueComponent,
|
||||
DialogueEditor)
|
||||
{
|
||||
registry.registerComponent<DialogueComponent>(
|
||||
DialogueComponent_name, DialogueComponent_group,
|
||||
std::make_unique<DialogueEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<DialogueComponent>()) {
|
||||
e.set<DialogueComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<DialogueComponent>()) {
|
||||
e.remove<DialogueComponent>();
|
||||
}
|
||||
});
|
||||
}
|
||||
21
src/features/editScene/components/EventHandler.hpp
Normal file
21
src/features/editScene/components/EventHandler.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef EDITSCENE_EVENT_HANDLER_HPP
|
||||
#define EDITSCENE_EVENT_HANDLER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Event-driven behavior tree handler component.
|
||||
*
|
||||
* When the specified event is received, the referenced GoapAction's
|
||||
* behavior tree is executed for this entity. Event parameters
|
||||
* (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
|
||||
21
src/features/editScene/components/EventHandlerModule.cpp
Normal file
21
src/features/editScene/components/EventHandlerModule.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "EventHandler.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/EventHandlerEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Event Handler", "Game", EventHandlerComponent,
|
||||
EventHandlerEditor)
|
||||
{
|
||||
registry.registerComponent<EventHandlerComponent>(
|
||||
"Event Handler", "Game",
|
||||
std::make_unique<EventHandlerEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<EventHandlerComponent>())
|
||||
e.set<EventHandlerComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<EventHandlerComponent>())
|
||||
e.remove<EventHandlerComponent>();
|
||||
});
|
||||
}
|
||||
739
src/features/editScene/components/EventParams.hpp
Normal file
739
src/features/editScene/components/EventParams.hpp
Normal 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
|
||||
11
src/features/editScene/components/GeneratedPhysicsTag.hpp
Normal file
11
src/features/editScene/components/GeneratedPhysicsTag.hpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#ifndef EDITSCENE_GENERATEDPHYSICSTAG_HPP
|
||||
#define EDITSCENE_GENERATEDPHYSICSTAG_HPP
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Marker component for entities auto-generated by systems (e.g. physics colliders).
|
||||
* Entities with this tag are hidden from the editor hierarchy and not serialized.
|
||||
*/
|
||||
struct GeneratedPhysicsTag {};
|
||||
|
||||
#endif // EDITSCENE_GENERATEDPHYSICSTAG_HPP
|
||||
74
src/features/editScene/components/GoapAction.hpp
Normal file
74
src/features/editScene/components/GoapAction.hpp
Normal 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
|
||||
227
src/features/editScene/components/GoapBlackboard.cpp
Normal file
227
src/features/editScene/components/GoapBlackboard.cpp
Normal 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 ®istry = 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;
|
||||
}
|
||||
236
src/features/editScene/components/GoapBlackboard.hpp
Normal file
236
src/features/editScene/components/GoapBlackboard.hpp
Normal 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
|
||||
21
src/features/editScene/components/GoapBlackboardModule.cpp
Normal file
21
src/features/editScene/components/GoapBlackboardModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
283
src/features/editScene/components/GoapExpression.cpp
Normal file
283
src/features/editScene/components/GoapExpression.cpp
Normal 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();
|
||||
}
|
||||
85
src/features/editScene/components/GoapExpression.hpp
Normal file
85
src/features/editScene/components/GoapExpression.hpp
Normal 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
|
||||
24
src/features/editScene/components/GoapGoal.cpp
Normal file
24
src/features/editScene/components/GoapGoal.cpp
Normal 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;
|
||||
}
|
||||
43
src/features/editScene/components/GoapGoal.hpp
Normal file
43
src/features/editScene/components/GoapGoal.hpp
Normal 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
|
||||
99
src/features/editScene/components/GoapPlanner.hpp
Normal file
99
src/features/editScene/components/GoapPlanner.hpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#ifndef EDITSCENE_GOAP_PLANNER_HPP
|
||||
#define EDITSCENE_GOAP_PLANNER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <queue>
|
||||
|
||||
/**
|
||||
* GOAP Planner component.
|
||||
*
|
||||
* Holds a curated list of action and goal names from an ActionDatabase,
|
||||
* plus configuration for smart-object action discovery.
|
||||
* The planner resolves names against an ActionDatabase at runtime.
|
||||
*
|
||||
* The actionNames and goalNames lists act as external references:
|
||||
* prefabs can store them even when the ActionDatabase is not
|
||||
* part of the prefab itself.
|
||||
*/
|
||||
struct GoapPlannerComponent {
|
||||
// Selected action names from ActionDatabase
|
||||
std::vector<Ogre::String> actionNames;
|
||||
|
||||
// Selected goal names from ActionDatabase
|
||||
std::vector<Ogre::String> goalNames;
|
||||
|
||||
// Maximum distance to search for smart objects with matching actions
|
||||
float smartObjectDistance = 50.0f;
|
||||
|
||||
// Whether to include smart object actions in planning
|
||||
bool includeSmartObjects = true;
|
||||
|
||||
// Optional reference to an external ActionDatabase entity by name.
|
||||
Ogre::String actionDatabaseRef;
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Runtime plan queue (not serialized)
|
||||
// -----------------------------------------------------------------
|
||||
struct Plan {
|
||||
std::vector<Ogre::String> actions;
|
||||
int totalCost = 0;
|
||||
Ogre::String goalName;
|
||||
};
|
||||
std::vector<Plan> planQueue;
|
||||
|
||||
// Planner status
|
||||
enum class Status {
|
||||
Idle, // No planning requested
|
||||
Planning, // Currently planning
|
||||
PlansAvailable, // One or more plans in queue
|
||||
NoPlanFound // Planning finished but no valid plan found
|
||||
};
|
||||
Status status = Status::Idle;
|
||||
|
||||
// Goal name that was used for the current plan batch
|
||||
Ogre::String currentGoalName;
|
||||
|
||||
// Planning control
|
||||
bool planDirty = true;
|
||||
int maxPlans = 3; // stop after generating this many plans
|
||||
|
||||
// Planning progress (for status display)
|
||||
int plansGenerated = 0;
|
||||
int nodesExplored = 0;
|
||||
|
||||
GoapPlannerComponent() = default;
|
||||
|
||||
void clearPlans()
|
||||
{
|
||||
planQueue.clear();
|
||||
plansGenerated = 0;
|
||||
status = Status::Idle;
|
||||
}
|
||||
|
||||
// Pop the cheapest plan from the queue
|
||||
Plan popCheapestPlan()
|
||||
{
|
||||
if (planQueue.empty())
|
||||
return Plan();
|
||||
size_t bestIdx = 0;
|
||||
for (size_t i = 1; i < planQueue.size(); i++) {
|
||||
if (planQueue[i].totalCost < planQueue[bestIdx].totalCost)
|
||||
bestIdx = i;
|
||||
}
|
||||
Plan result = std::move(planQueue[bestIdx]);
|
||||
planQueue.erase(planQueue.begin() + bestIdx);
|
||||
if (planQueue.empty() && status == Status::PlansAvailable)
|
||||
status = Status::Idle;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool hasPlans() const
|
||||
{
|
||||
return !planQueue.empty();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_PLANNER_HPP
|
||||
21
src/features/editScene/components/GoapPlannerModule.cpp
Normal file
21
src/features/editScene/components/GoapPlannerModule.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "GoapPlanner.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/GoapPlannerEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("GOAP Planner", "AI", GoapPlannerComponent,
|
||||
GoapPlannerEditor)
|
||||
{
|
||||
registry.registerComponent<GoapPlannerComponent>(
|
||||
"GOAP Planner", "AI",
|
||||
std::make_unique<GoapPlannerEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<GoapPlannerComponent>())
|
||||
e.set<GoapPlannerComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<GoapPlannerComponent>())
|
||||
e.remove<GoapPlannerComponent>();
|
||||
});
|
||||
}
|
||||
50
src/features/editScene/components/GoapRunner.hpp
Normal file
50
src/features/editScene/components/GoapRunner.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef EDITSCENE_GOAP_RUNNER_HPP
|
||||
#define EDITSCENE_GOAP_RUNNER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* GOAP Runner component.
|
||||
*
|
||||
* Executes plans from GoapPlannerComponent.
|
||||
* For normal actions: runs the action's behavior tree.
|
||||
* For smart object actions: pathfinds to the smart object and executes.
|
||||
* After plan completion, marks the planner dirty for replanning.
|
||||
*/
|
||||
struct GoapRunnerComponent {
|
||||
// Current plan execution state
|
||||
enum class State {
|
||||
Idle, // No plan running
|
||||
RunningAction, // Executing a normal action
|
||||
MovingToSmartObject, // Pathfinding to a smart object
|
||||
ExecutingSmartObject, // Executing smart object action
|
||||
PlanComplete // Plan finished, waiting for replan
|
||||
};
|
||||
|
||||
State state = State::Idle;
|
||||
|
||||
// Index of current action in the plan
|
||||
int currentActionIndex = 0;
|
||||
|
||||
// Name of the currently executing action
|
||||
Ogre::String currentActionName;
|
||||
|
||||
// Timer for action execution
|
||||
float actionTimer = 0.0f;
|
||||
|
||||
// Entity ID of target smart object (if applicable)
|
||||
uint64_t targetSmartObjectId = 0;
|
||||
|
||||
// Active plan actions (copied from planner when plan starts)
|
||||
std::vector<Ogre::String> planActions;
|
||||
|
||||
// Whether to auto-replan after completion
|
||||
bool autoReplan = true;
|
||||
|
||||
GoapRunnerComponent() = default;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_RUNNER_HPP
|
||||
21
src/features/editScene/components/GoapRunnerModule.cpp
Normal file
21
src/features/editScene/components/GoapRunnerModule.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "GoapRunner.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/GoapRunnerEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("GOAP Runner", "AI", GoapRunnerComponent,
|
||||
GoapRunnerEditor)
|
||||
{
|
||||
registry.registerComponent<GoapRunnerComponent>(
|
||||
"GOAP Runner", "AI",
|
||||
std::make_unique<GoapRunnerEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<GoapRunnerComponent>())
|
||||
e.set<GoapRunnerComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<GoapRunnerComponent>())
|
||||
e.remove<GoapRunnerComponent>();
|
||||
});
|
||||
}
|
||||
12
src/features/editScene/components/InWater.hpp
Normal file
12
src/features/editScene/components/InWater.hpp
Normal 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
|
||||
165
src/features/editScene/components/Inventory.hpp
Normal file
165
src/features/editScene/components/Inventory.hpp
Normal file
@@ -0,0 +1,165 @@
|
||||
#ifndef EDITSCENE_INVENTORY_HPP
|
||||
#define EDITSCENE_INVENTORY_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* A single slot in an inventory.
|
||||
* Stores a reference to an item entity (if the item is a world entity)
|
||||
* or stores item data directly for items that exist only in inventory.
|
||||
*/
|
||||
struct InventorySlot {
|
||||
// Flecs entity ID of the item (0 if slot is empty)
|
||||
flecs::entity_t itemEntity = 0;
|
||||
|
||||
// Item data for items that exist only in inventory (no world entity)
|
||||
Ogre::String itemId;
|
||||
Ogre::String itemName;
|
||||
Ogre::String itemType;
|
||||
int stackSize = 0;
|
||||
int maxStackSize = 99;
|
||||
float weight = 0.1f;
|
||||
int value = 1;
|
||||
Ogre::String useActionName;
|
||||
|
||||
bool isEmpty() const
|
||||
{
|
||||
return itemEntity == 0 && stackSize <= 0;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
itemEntity = 0;
|
||||
itemId.clear();
|
||||
itemName.clear();
|
||||
itemType.clear();
|
||||
stackSize = 0;
|
||||
maxStackSize = 99;
|
||||
weight = 0.1f;
|
||||
value = 1;
|
||||
useActionName.clear();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Inventory component.
|
||||
*
|
||||
* Attached to a character entity to hold items.
|
||||
* Can also be attached to container entities (chests, barrels, etc.)
|
||||
* to define their contents.
|
||||
*
|
||||
* The inventory stores items as InventorySlot entries, each of which
|
||||
* may reference a world ItemComponent entity or hold item data directly.
|
||||
*/
|
||||
struct InventoryComponent {
|
||||
// Maximum number of slots
|
||||
int maxSlots = 20;
|
||||
|
||||
// Current slots
|
||||
std::vector<InventorySlot> slots;
|
||||
|
||||
// Total weight of all items (computed)
|
||||
float totalWeight = 0.0f;
|
||||
|
||||
// Maximum weight capacity (0 = unlimited)
|
||||
float maxWeight = 50.0f;
|
||||
|
||||
// Whether this inventory is a container (chest, barrel, etc.)
|
||||
// Containers can be opened by characters to transfer items.
|
||||
bool isContainer = false;
|
||||
|
||||
// Whether this inventory is currently open (for containers being browsed)
|
||||
bool isOpen = false;
|
||||
|
||||
InventoryComponent() = default;
|
||||
|
||||
explicit InventoryComponent(int maxSlots_)
|
||||
: maxSlots(maxSlots_)
|
||||
{
|
||||
slots.reserve(maxSlots_);
|
||||
}
|
||||
|
||||
/** Find the first empty slot index, or -1 if full. */
|
||||
int findEmptySlot() const
|
||||
{
|
||||
for (int i = 0; i < maxSlots; i++) {
|
||||
if (i >= (int)slots.size())
|
||||
return i;
|
||||
if (slots[i].isEmpty())
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Find a slot containing an item with the given itemId. */
|
||||
int findItem(const Ogre::String &itemId) const
|
||||
{
|
||||
for (int i = 0; i < (int)slots.size(); i++) {
|
||||
if (!slots[i].isEmpty() && slots[i].itemId == itemId)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Find a slot containing an item with the given itemName. */
|
||||
int findItemByName(const Ogre::String &itemName) const
|
||||
{
|
||||
for (int i = 0; i < (int)slots.size(); i++) {
|
||||
if (!slots[i].isEmpty() &&
|
||||
slots[i].itemName == itemName)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Count total number of items (sum of stack sizes). */
|
||||
int countItems() const
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty())
|
||||
count += slot.stackSize;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Count how many of a specific itemId are in the inventory. */
|
||||
int countItem(const Ogre::String &itemId) const
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty() && slot.itemId == itemId)
|
||||
count += slot.stackSize;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Check if inventory has at least one of a specific itemId. */
|
||||
bool hasItem(const Ogre::String &itemId) const
|
||||
{
|
||||
return findItem(itemId) >= 0;
|
||||
}
|
||||
|
||||
/** Check if inventory has at least one of a specific itemName. */
|
||||
bool hasItemByName(const Ogre::String &itemName) const
|
||||
{
|
||||
return findItemByName(itemName) >= 0;
|
||||
}
|
||||
|
||||
/** Recalculate total weight. */
|
||||
void recalculateWeight()
|
||||
{
|
||||
totalWeight = 0.0f;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty())
|
||||
totalWeight += slot.weight * slot.stackSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_INVENTORY_HPP
|
||||
20
src/features/editScene/components/InventoryModule.cpp
Normal file
20
src/features/editScene/components/InventoryModule.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "Inventory.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/InventoryEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Inventory", "Game", InventoryComponent,
|
||||
InventoryEditor)
|
||||
{
|
||||
registry.registerComponent<InventoryComponent>(
|
||||
"Inventory", "Game", std::make_unique<InventoryEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<InventoryComponent>())
|
||||
e.set<InventoryComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<InventoryComponent>())
|
||||
e.remove<InventoryComponent>();
|
||||
});
|
||||
}
|
||||
59
src/features/editScene/components/Item.hpp
Normal file
59
src/features/editScene/components/Item.hpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifndef EDITSCENE_ITEM_HPP
|
||||
#define EDITSCENE_ITEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Item definition component.
|
||||
*
|
||||
* Attached to a world entity that represents a pickable item.
|
||||
* The ActuatorSystem detects items (entities with ItemComponent)
|
||||
* and shows "E - Pick up [ItemName]" prompts to the player.
|
||||
*
|
||||
* Items can also be placed in containers (chests, etc.) which
|
||||
* have an InventoryComponent.
|
||||
*
|
||||
* For AI characters, behavior tree nodes (hasItem, pickupItem,
|
||||
* dropItem, useItem, addItemToInventory) provide inventory access.
|
||||
*/
|
||||
struct ItemComponent {
|
||||
// Display name of the item (e.g. "Apple", "Sword", "Key")
|
||||
Ogre::String itemName = "Item";
|
||||
|
||||
// Item type for categorization (e.g. "food", "weapon", "key", "quest")
|
||||
Ogre::String itemType = "misc";
|
||||
|
||||
// Unique identifier for this item definition
|
||||
// Multiple entities can share the same itemId (e.g. multiple coins)
|
||||
Ogre::String itemId;
|
||||
|
||||
// Stack size: how many of this item are in this stack
|
||||
int stackSize = 1;
|
||||
|
||||
// Maximum stack size (0 = no stacking)
|
||||
int maxStackSize = 99;
|
||||
|
||||
// Weight per unit (for encumbrance calculations)
|
||||
float weight = 0.1f;
|
||||
|
||||
// Value (for trading)
|
||||
int value = 1;
|
||||
|
||||
// Name of the GOAP action to execute when "using" this item
|
||||
// (e.g. "eat", "equip", "read"). Empty = no use action.
|
||||
Ogre::String useActionName;
|
||||
|
||||
ItemComponent() = default;
|
||||
|
||||
explicit ItemComponent(const Ogre::String &name,
|
||||
const Ogre::String &type = "misc")
|
||||
: itemName(name)
|
||||
, itemType(type)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ITEM_HPP
|
||||
19
src/features/editScene/components/ItemModule.cpp
Normal file
19
src/features/editScene/components/ItemModule.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "Item.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ItemEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Item", "Game", ItemComponent, ItemEditor)
|
||||
{
|
||||
registry.registerComponent<ItemComponent>(
|
||||
"Item", "Game", std::make_unique<ItemEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ItemComponent>())
|
||||
e.set<ItemComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ItemComponent>())
|
||||
e.remove<ItemComponent>();
|
||||
});
|
||||
}
|
||||
59
src/features/editScene/components/NavMesh.hpp
Normal file
59
src/features/editScene/components/NavMesh.hpp
Normal 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
|
||||
38
src/features/editScene/components/NavMeshModule.cpp
Normal file
38
src/features/editScene/components/NavMeshModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
77
src/features/editScene/components/PathFollowing.hpp
Normal file
77
src/features/editScene/components/PathFollowing.hpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#ifndef EDITSCENE_PATH_FOLLOWING_HPP
|
||||
#define EDITSCENE_PATH_FOLLOWING_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* Animation state configuration for path following.
|
||||
*
|
||||
* Each state (e.g. "idle", "walk", "run") consists of unlimited
|
||||
* state-machine-name -> state-name pairs applied via AnimationTreeSystem.
|
||||
*/
|
||||
struct PathFollowingState {
|
||||
Ogre::String name;
|
||||
std::vector<std::pair<Ogre::String, Ogre::String> > stateMachineStates;
|
||||
|
||||
PathFollowingState() = default;
|
||||
explicit PathFollowingState(const Ogre::String &name_)
|
||||
: name(name_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Path Following component.
|
||||
*
|
||||
* Configures animation states for locomotion (idle/walk/run)
|
||||
* and stores the current locomotion state. Used by GoapRunner
|
||||
* to animate characters during plan execution.
|
||||
*/
|
||||
struct PathFollowingComponent {
|
||||
// Animation state configurations
|
||||
std::vector<PathFollowingState> pathFollowingStates = {
|
||||
PathFollowingState("idle"),
|
||||
PathFollowingState("walk"),
|
||||
PathFollowingState("run"),
|
||||
};
|
||||
|
||||
// Current locomotion state name
|
||||
Ogre::String currentLocomotionState = "idle";
|
||||
|
||||
// Walk speed (m/s) for root motion scaling
|
||||
float walkSpeed = 2.5f;
|
||||
|
||||
// Run speed (m/s) for root motion scaling
|
||||
float runSpeed = 5.0f;
|
||||
|
||||
// Whether to use root motion
|
||||
bool useRootMotion = true;
|
||||
|
||||
// Target position for path following (set by GoapRunner)
|
||||
Ogre::Vector3 targetPosition = Ogre::Vector3::ZERO;
|
||||
|
||||
// Whether we have an active target
|
||||
bool hasTarget = false;
|
||||
|
||||
// Path waypoints (set by GoapRunner, followed by PathFollowingSystem)
|
||||
std::vector<Ogre::Vector3> path;
|
||||
int pathIndex = 0;
|
||||
float pathRecalcTimer = 0.0f;
|
||||
|
||||
PathFollowingComponent() = default;
|
||||
|
||||
const PathFollowingState *findState(const Ogre::String &name) const
|
||||
{
|
||||
for (const auto &state : pathFollowingStates) {
|
||||
if (state.name == name)
|
||||
return &state;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_PATH_FOLLOWING_HPP
|
||||
21
src/features/editScene/components/PathFollowingModule.cpp
Normal file
21
src/features/editScene/components/PathFollowingModule.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "PathFollowing.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/PathFollowingEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Path Following", "AI", PathFollowingComponent,
|
||||
PathFollowingEditor)
|
||||
{
|
||||
registry.registerComponent<PathFollowingComponent>(
|
||||
"Path Following", "AI",
|
||||
std::make_unique<PathFollowingEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<PathFollowingComponent>())
|
||||
e.set<PathFollowingComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<PathFollowingComponent>())
|
||||
e.remove<PathFollowingComponent>();
|
||||
});
|
||||
}
|
||||
42
src/features/editScene/components/PlayerController.hpp
Normal file
42
src/features/editScene/components/PlayerController.hpp
Normal 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
|
||||
24
src/features/editScene/components/PlayerControllerModule.cpp
Normal file
24
src/features/editScene/components/PlayerControllerModule.cpp
Normal 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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
24
src/features/editScene/components/PrefabInstance.hpp
Normal file
24
src/features/editScene/components/PrefabInstance.hpp
Normal 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
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <string>
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
#include "ProceduralTexture.hpp"
|
||||
|
||||
/**
|
||||
* @brief Component for creating procedural Ogre materials
|
||||
@@ -11,29 +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;
|
||||
|
||||
// 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>();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ProceduralMaterialEditor.hpp"
|
||||
#include <OgreMaterialManager.h>
|
||||
#include <OgreRTShaderSystem.h>
|
||||
|
||||
// Register ProceduralMaterial component
|
||||
REGISTER_COMPONENT("Procedural Material", ProceduralMaterialComponent, ProceduralMaterialEditor)
|
||||
REGISTER_COMPONENT_GROUP("Procedural Material", "Material", ProceduralMaterialComponent, ProceduralMaterialEditor)
|
||||
{
|
||||
registry.registerComponent<ProceduralMaterialComponent>(
|
||||
"Procedural Material",
|
||||
"Procedural Material", "Material",
|
||||
std::make_unique<ProceduralMaterialEditor>(sceneMgr),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
@@ -22,6 +23,12 @@ REGISTER_COMPONENT("Procedural Material", ProceduralMaterialComponent, Procedura
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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,119 +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;
|
||||
|
||||
// Whether the texture needs regeneration
|
||||
bool dirty = true;
|
||||
|
||||
// Whether the texture has been generated
|
||||
bool generated = false;
|
||||
|
||||
// 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);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove a named rectangle
|
||||
bool removeNamedRect(const std::string& name) {
|
||||
auto it = namedRects.find(name);
|
||||
if (it != namedRects.end()) {
|
||||
namedRects.erase(it);
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
#include "../ui/ProceduralTextureEditor.hpp"
|
||||
|
||||
// Register ProceduralTexture component
|
||||
REGISTER_COMPONENT("Procedural Texture", ProceduralTextureComponent, ProceduralTextureEditor)
|
||||
REGISTER_COMPONENT_GROUP("Procedural Texture", "Material", ProceduralTextureComponent, ProceduralTextureEditor)
|
||||
{
|
||||
registry.registerComponent<ProceduralTextureComponent>(
|
||||
"Procedural Texture",
|
||||
"Procedural Texture", "Material",
|
||||
std::make_unique<ProceduralTextureEditor>(),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
|
||||
58
src/features/editScene/components/Skybox.hpp
Normal file
58
src/features/editScene/components/Skybox.hpp
Normal 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
|
||||
24
src/features/editScene/components/SkyboxModule.cpp
Normal file
24
src/features/editScene/components/SkyboxModule.cpp
Normal 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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
38
src/features/editScene/components/SmartObject.hpp
Normal file
38
src/features/editScene/components/SmartObject.hpp
Normal 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
|
||||
20
src/features/editScene/components/SmartObjectModule.cpp
Normal file
20
src/features/editScene/components/SmartObjectModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
20
src/features/editScene/components/StartupMenu.hpp
Normal file
20
src/features/editScene/components/StartupMenu.hpp
Normal 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
|
||||
23
src/features/editScene/components/StartupMenuModule.cpp
Normal file
23
src/features/editScene/components/StartupMenuModule.cpp
Normal 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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
70
src/features/editScene/components/Sun.hpp
Normal file
70
src/features/editScene/components/Sun.hpp
Normal 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
|
||||
24
src/features/editScene/components/SunModule.cpp
Normal file
24
src/features/editScene/components/SunModule.cpp
Normal 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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
43
src/features/editScene/components/WaterPhysics.hpp
Normal file
43
src/features/editScene/components/WaterPhysics.hpp
Normal 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
|
||||
22
src/features/editScene/components/WaterPhysicsModule.cpp
Normal file
22
src/features/editScene/components/WaterPhysicsModule.cpp
Normal 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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
73
src/features/editScene/components/WaterPlane.hpp
Normal file
73
src/features/editScene/components/WaterPlane.hpp
Normal 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
|
||||
25
src/features/editScene/components/WaterPlaneModule.cpp
Normal file
25
src/features/editScene/components/WaterPlaneModule.cpp
Normal 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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
409
src/features/editScene/gizmo/Cursor3D.cpp
Normal file
409
src/features/editScene/gizmo/Cursor3D.cpp
Normal 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;
|
||||
}
|
||||
96
src/features/editScene/gizmo/Cursor3D.hpp
Normal file
96
src/features/editScene/gizmo/Cursor3D.hpp
Normal 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
|
||||
418
src/features/editScene/lua-examples/action_db_example.lua
Normal file
418
src/features/editScene/lua-examples/action_db_example.lua
Normal file
@@ -0,0 +1,418 @@
|
||||
-- =============================================================================
|
||||
-- Action Database Lua API Examples
|
||||
-- =============================================================================
|
||||
-- This file demonstrates how to create, query, and manage GOAP actions and
|
||||
-- goals from Lua using the ecs.action_db API.
|
||||
--
|
||||
-- The ActionDatabase is a global singleton. Actions and goals defined here
|
||||
-- are immediately available to all characters in the scene.
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Defining Bit Names
|
||||
-- =============================================================================
|
||||
-- Before using bits in preconditions/effects, you should define meaningful
|
||||
-- names for the 64 available bit slots. This makes your actions readable.
|
||||
--
|
||||
-- Bit names are global across the entire game session. They map
|
||||
-- human-readable names (like "has_axe", "is_hungry") to bit indices
|
||||
-- (0-63) used in GoapBlackboard preconditions and effects.
|
||||
--
|
||||
-- You can define bits explicitly at startup:
|
||||
-- =============================================================================
|
||||
|
||||
-- Explicitly assign bit names to specific indices:
|
||||
ecs.action_db.set_bit_name(0, "has_axe")
|
||||
ecs.action_db.set_bit_name(1, "has_wood")
|
||||
ecs.action_db.set_bit_name(2, "is_hungry")
|
||||
ecs.action_db.set_bit_name(3, "near_tree")
|
||||
ecs.action_db.set_bit_name(4, "near_well")
|
||||
ecs.action_db.set_bit_name(5, "has_bucket")
|
||||
ecs.action_db.set_bit_name(6, "has_food")
|
||||
ecs.action_db.set_bit_name(7, "near_fire")
|
||||
ecs.action_db.set_bit_name(8, "has_cooked_food")
|
||||
ecs.action_db.set_bit_name(9, "is_awake")
|
||||
ecs.action_db.set_bit_name(10, "at_market")
|
||||
ecs.action_db.set_bit_name(11, "at_home")
|
||||
ecs.action_db.set_bit_name(12, "near_chair")
|
||||
ecs.action_db.set_bit_name(13, "is_sitting")
|
||||
ecs.action_db.set_bit_name(14, "near_forest")
|
||||
ecs.action_db.set_bit_name(15, "is_strong")
|
||||
ecs.action_db.set_bit_name(16, "has_strength")
|
||||
|
||||
-- Or use auto_assign_bit() to let the system pick the index:
|
||||
local idx = ecs.action_db.auto_assign_bit("has_water")
|
||||
print("'has_water' assigned to bit " .. idx)
|
||||
|
||||
-- Look up a bit by name:
|
||||
local bit_idx = ecs.action_db.find_bit_by_name("has_axe")
|
||||
print("'has_axe' is at bit " .. bit_idx)
|
||||
|
||||
-- Get the name for a bit index:
|
||||
local name = ecs.action_db.get_bit_name(0)
|
||||
print("Bit 0 is named '" .. name .. "'")
|
||||
|
||||
-- List all currently assigned bit names:
|
||||
local bits = ecs.action_db.list_bit_names()
|
||||
print("Assigned bit names:")
|
||||
for _, b in ipairs(bits) do
|
||||
print(" Bit " .. b.index .. ": " .. b.name)
|
||||
end
|
||||
|
||||
-- NOTE: If you use a bit name in an action's preconditions/effects
|
||||
-- that hasn't been explicitly assigned, it will be auto-assigned
|
||||
-- to the first free slot automatically. So you don't HAVE to
|
||||
-- pre-define them, but it's good practice for clarity.
|
||||
|
||||
-- =============================================================================
|
||||
-- Creating Actions
|
||||
-- =============================================================================
|
||||
|
||||
-- Simple action with just a name and cost:
|
||||
ecs.action_db.add_action("idle", 1)
|
||||
|
||||
-- Action with preconditions (what must be true before the action can run):
|
||||
ecs.action_db.add_action("chop_wood", 2,
|
||||
{
|
||||
bits = { has_axe = true },
|
||||
values = { stamina = 10 }
|
||||
},
|
||||
{ -- effects (what becomes true after the action runs)
|
||||
bits = { has_wood = true },
|
||||
values = { stamina = -5 }
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with only preconditions, no effects:
|
||||
ecs.action_db.add_action("fetch_water", 3,
|
||||
{
|
||||
bits = { near_well = true, has_bucket = true },
|
||||
values = { thirst = 50 }
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with only effects, no preconditions:
|
||||
ecs.action_db.add_action("rest", 1,
|
||||
{},
|
||||
{
|
||||
values = { stamina = 100, energy = 100 }
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with float and string values in blackboard:
|
||||
ecs.action_db.add_action("cook_food", 4,
|
||||
{
|
||||
bits = { has_food = true, near_fire = true },
|
||||
values = { cooking_skill = 3 },
|
||||
floatValues = { hunger = 50.0 }
|
||||
},
|
||||
{
|
||||
bits = { has_cooked_food = true },
|
||||
values = { hunger = -30 },
|
||||
stringValues = { last_action = "cooking" }
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Creating Actions WITH Behavior Trees
|
||||
-- =============================================================================
|
||||
|
||||
-- Action with a simple leaf behavior tree (plays an animation):
|
||||
ecs.action_db.add_action("wave", 1,
|
||||
{}, -- no preconditions
|
||||
{}, -- no effects
|
||||
{ -- behavior tree (arg 5)
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "setAnimationState", name = "SM/Wave" },
|
||||
{ type = "delay", params = "2.0" },
|
||||
{ type = "setAnimationState", name = "SM/Idle" }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with a selector behavior tree (try to chop, fall back to idle):
|
||||
ecs.action_db.add_action("chop_tree", 3,
|
||||
{
|
||||
bits = { near_tree = true },
|
||||
values = { stamina = 15 }
|
||||
},
|
||||
{
|
||||
bits = { has_wood = true },
|
||||
values = { stamina = -8, wood_count = 1 }
|
||||
},
|
||||
{
|
||||
type = "selector",
|
||||
children = {
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "checkBit", name = "has_axe", params = "1" },
|
||||
{ type = "setAnimationState", name = "SM/Chop" },
|
||||
{ type = "delay", params = "3.0" },
|
||||
{ type = "setAnimationState", name = "SM/Idle" },
|
||||
{ type = "setBit", name = "has_wood", params = "1" }
|
||||
}
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "debugPrint", name = "No axe! Picking up stick..." },
|
||||
{ type = "setAnimationState", name = "SM/Pickup" },
|
||||
{ type = "delay", params = "1.0" },
|
||||
{ type = "setBit", name = "has_wood", params = "1" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with blackboard checks and value manipulation:
|
||||
ecs.action_db.add_action("travel_to_market", 5,
|
||||
{
|
||||
bits = { is_awake = true },
|
||||
values = { energy = 20 }
|
||||
},
|
||||
{
|
||||
bits = { at_market = true },
|
||||
values = { energy = -15 }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "checkValue", name = "energy", params = ">= 20" },
|
||||
{ type = "setAnimationState", name = "SM/Walk" },
|
||||
{ type = "delay", params = "5.0" },
|
||||
{ type = "setAnimationState", name = "SM/Idle" },
|
||||
{ type = "setBit", name = "at_market", params = "1" },
|
||||
{ type = "setBit", name = "at_home", params = "0" }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- Action with inventory operations:
|
||||
ecs.action_db.add_action("gather_wood", 2,
|
||||
{
|
||||
bits = { near_forest = true }
|
||||
},
|
||||
{
|
||||
values = { wood_count = 3 }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "setAnimationState", name = "SM/Gather" },
|
||||
{ type = "delay", params = "2.0" },
|
||||
{ type = "addItemToInventory", params = "wood,Firewood,material,3,1.0,0" },
|
||||
{ type = "setAnimationState", name = "SM/Idle" }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- Action that teleports character to a smart object child:
|
||||
ecs.action_db.add_action("sit_on_chair", 1,
|
||||
{
|
||||
bits = { near_chair = true }
|
||||
},
|
||||
{
|
||||
bits = { is_sitting = true }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
{ type = "teleportToChild", name = "SitTarget" },
|
||||
{ type = "disablePhysics" },
|
||||
{ type = "setAnimationState", name = "SM/Sit" },
|
||||
{ type = "delay", params = "5.0" },
|
||||
{ type = "setAnimationState", name = "SM/Stand" },
|
||||
{ type = "enablePhysics" }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Creating Goals
|
||||
-- =============================================================================
|
||||
|
||||
-- Simple goal with just a name and priority:
|
||||
ecs.action_db.add_goal("survive", 100)
|
||||
|
||||
-- Goal with a target blackboard state:
|
||||
ecs.action_db.add_goal("gather_resources", 50,
|
||||
{
|
||||
bits = { has_wood = true, has_water = true },
|
||||
values = { wood_count = 5, water_count = 3 }
|
||||
}
|
||||
)
|
||||
|
||||
-- Goal with a condition expression (evaluated against character's blackboard):
|
||||
ecs.action_db.add_goal("stay_healthy", 80,
|
||||
{
|
||||
values = { health = 100, stamina = 80 }
|
||||
},
|
||||
"health < 50 || stamina < 30" -- only valid when character needs healing
|
||||
)
|
||||
|
||||
-- Goal with full specification:
|
||||
ecs.action_db.add_goal("become_strong", 30,
|
||||
{
|
||||
bits = { is_strong = true },
|
||||
values = { strength = 100 }
|
||||
},
|
||||
"strength < 100" -- only valid if not already strong
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Querying Actions and Goals
|
||||
-- =============================================================================
|
||||
|
||||
-- Find an action by name:
|
||||
local action = ecs.action_db.find_action("chop_wood")
|
||||
if action then
|
||||
print("Found action: " .. action.name .. " (cost: " .. action.cost .. ")")
|
||||
-- action.preconditions and action.effects are tables with:
|
||||
-- .bits - table of boolean flags
|
||||
-- .values - table of integer values
|
||||
-- .floatValues - table of float values
|
||||
-- .stringValues - table of string values
|
||||
-- action.behaviorTree is a table with:
|
||||
-- .type - node type string
|
||||
-- .name - optional name
|
||||
-- .params - optional params
|
||||
-- .children - optional array of child nodes
|
||||
end
|
||||
|
||||
-- Find a goal by name:
|
||||
local goal = ecs.action_db.find_goal("gather_resources")
|
||||
if goal then
|
||||
print("Found goal: " .. goal.name .. " (priority: " .. goal.priority .. ")")
|
||||
-- goal.target is a blackboard table
|
||||
-- goal.condition is the condition string
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Listing All Actions and Goals
|
||||
-- =============================================================================
|
||||
|
||||
-- List all action names:
|
||||
local actions = ecs.action_db.list_actions()
|
||||
print("Available actions:")
|
||||
for i, name in ipairs(actions) do
|
||||
print(" " .. i .. ". " .. name)
|
||||
end
|
||||
|
||||
-- List all goal names:
|
||||
local goals = ecs.action_db.list_goals()
|
||||
print("Available goals:")
|
||||
for i, name in ipairs(goals) do
|
||||
print(" " .. i .. ". " .. name)
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Removing Actions and Goals
|
||||
-- =============================================================================
|
||||
|
||||
-- Remove an action by name:
|
||||
local removed = ecs.action_db.remove_action("idle")
|
||||
if removed then
|
||||
print("Removed action: idle")
|
||||
end
|
||||
|
||||
-- Remove a goal by name:
|
||||
ecs.action_db.remove_goal("become_strong")
|
||||
|
||||
-- =============================================================================
|
||||
-- Replacing Actions (same name = replace)
|
||||
-- =============================================================================
|
||||
|
||||
-- If you add an action with the same name as an existing one, it replaces it:
|
||||
ecs.action_db.add_action("chop_wood", 5,
|
||||
{
|
||||
bits = { has_axe = true, has_strength = true },
|
||||
values = { stamina = 20 }
|
||||
},
|
||||
{
|
||||
bits = { has_wood = true },
|
||||
values = { stamina = -10, wood_count = 2 }
|
||||
}
|
||||
)
|
||||
-- The old "chop_wood" action is replaced with this new definition.
|
||||
|
||||
-- =============================================================================
|
||||
-- Clearing Everything
|
||||
-- =============================================================================
|
||||
|
||||
-- Remove all actions and goals:
|
||||
-- ecs.action_db.clear()
|
||||
|
||||
-- =============================================================================
|
||||
-- Blackboard Table Format Reference
|
||||
-- =============================================================================
|
||||
--
|
||||
-- The blackboard table passed to add_action/add_goal has this structure:
|
||||
--
|
||||
-- {
|
||||
-- bits = {
|
||||
-- has_axe = true, -- boolean flags (use named bits)
|
||||
-- has_wood = false,
|
||||
-- is_hungry = true
|
||||
-- },
|
||||
-- values = { -- integer values
|
||||
-- health = 100,
|
||||
-- stamina = 50,
|
||||
-- wood_count = 0
|
||||
-- },
|
||||
-- floatValues = { -- float values
|
||||
-- hunger = 75.5,
|
||||
-- speed = 1.2
|
||||
-- },
|
||||
-- stringValues = { -- string values
|
||||
-- last_action = "idle",
|
||||
-- current_state = "exploring"
|
||||
-- }
|
||||
-- }
|
||||
--
|
||||
-- All sub-tables are optional. An empty table or nil means no constraints.
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Behavior Tree Table Format Reference
|
||||
-- =============================================================================
|
||||
--
|
||||
-- The behaviorTree table (arg 5 of add_action) has this structure:
|
||||
--
|
||||
-- {
|
||||
-- type = "sequence", -- node type (required)
|
||||
-- name = "optional_name", -- depends on type (task name, anim state, etc.)
|
||||
-- params = "optional_params", -- extra parameters (delay seconds, bit index, etc.)
|
||||
-- children = { -- array of child nodes (for sequence/selector/invert)
|
||||
-- { type = "task", name = "myAction" },
|
||||
-- { type = "setAnimationState", name = "SM/Walk" },
|
||||
-- { type = "delay", params = "2.0" },
|
||||
-- { type = "checkBit", name = "has_axe", params = "1" }
|
||||
-- }
|
||||
-- }
|
||||
--
|
||||
-- Common node types:
|
||||
-- "sequence" - Execute children in order until one fails
|
||||
-- "selector" - Execute children in order until one succeeds
|
||||
-- "invert" - Invert the result of a single child
|
||||
-- "task" - Leaf: references a named task
|
||||
-- "check" - Leaf: references a named condition
|
||||
-- "debugPrint" - Leaf: prints 'name' to console
|
||||
-- "setAnimationState"- Leaf: sets animation state (name="SM/State")
|
||||
-- "isAnimationEnded" - Leaf: true if animation ended
|
||||
-- "setBit" - Leaf: sets blackboard bit (name=bit, params=0/1)
|
||||
-- "checkBit" - Leaf: true if blackboard bit is set
|
||||
-- "setValue" - Leaf: sets blackboard value (name=key, params=val)
|
||||
-- "checkValue" - Leaf: blackboard comparison (name=key, params="op val")
|
||||
-- "delay" - Leaf: waits N seconds (params=seconds as float)
|
||||
-- "teleportToChild" - Leaf: teleports to named child of Smart Object
|
||||
-- "disablePhysics" - Leaf: removes character from physics
|
||||
-- "enablePhysics" - Leaf: re-adds character to physics
|
||||
-- "hasItem" - Leaf check: true if inventory has itemId
|
||||
-- "pickupItem" - Leaf: picks up nearest item
|
||||
-- "dropItem" - Leaf: drops item from inventory
|
||||
-- "useItem" - Leaf: uses item from inventory
|
||||
-- "addItemToInventory"- Leaf: adds item directly to inventory
|
||||
-- =============================================================================
|
||||
514
src/features/editScene/lua-examples/behavior_tree_example.lua
Normal file
514
src/features/editScene/lua-examples/behavior_tree_example.lua
Normal file
@@ -0,0 +1,514 @@
|
||||
-- =============================================================================
|
||||
-- Behavior Tree Lua API Examples
|
||||
-- =============================================================================
|
||||
-- This file demonstrates how to create custom behavior tree nodes using
|
||||
-- Lua functions via the ecs.behavior_tree API, and how to use the
|
||||
-- built-in C++ node types via ecs.behavior_tree.create_node().
|
||||
--
|
||||
-- The API allows you to:
|
||||
-- 1. Register Lua functions as behavior tree node handlers
|
||||
-- 2. Create behavior tree nodes (both Lua and built-in C++ types)
|
||||
-- 3. Return "success", "failure", or "running" to control tree flow
|
||||
-- 4. Pass parameters from the behavior tree editor to your Lua function
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Registering Lua Node Handlers
|
||||
-- =============================================================================
|
||||
-- Use ecs.behavior_tree.register_node(name, function) to register a Lua
|
||||
-- function as a behavior tree node handler.
|
||||
--
|
||||
-- The function receives two arguments:
|
||||
-- entity_id - The entity ID executing this behavior tree node
|
||||
-- params - A table of parameters parsed from the node's params string
|
||||
--
|
||||
-- The function must return one of:
|
||||
-- "success" - Node completed successfully (tree continues)
|
||||
-- "failure" - Node failed (tree stops with failure)
|
||||
-- "running" - Node is still running (will be called again next frame)
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 1: Simple greeting node
|
||||
-- =============================================================================
|
||||
-- Prints a message and succeeds immediately.
|
||||
|
||||
ecs.behavior_tree.register_node("say_hello", function(entity_id, params)
|
||||
local message = params.message or "Hello!"
|
||||
print("Entity " .. entity_id .. " says: " .. message)
|
||||
return "success"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 2: Node that checks a blackboard value
|
||||
-- =============================================================================
|
||||
-- Checks if a blackboard integer value meets a minimum threshold.
|
||||
-- Succeeds if value >= min, fails otherwise.
|
||||
|
||||
ecs.behavior_tree.register_node("check_blackboard_value", function(entity_id, params)
|
||||
local key = params.key
|
||||
local min_val = tonumber(params.min) or 0
|
||||
|
||||
if not key then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local value = bb.values[key]
|
||||
if value == nil then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
if value >= min_val then
|
||||
return "success"
|
||||
else
|
||||
return "failure"
|
||||
end
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 3: Node that runs over multiple frames (running state)
|
||||
-- =============================================================================
|
||||
-- Waits for a specified duration, storing progress in the blackboard's
|
||||
-- floatValues. Returns "running" each frame until the duration elapses.
|
||||
|
||||
ecs.behavior_tree.register_node("wait_for_duration", function(entity_id, params)
|
||||
local duration = tonumber(params.duration) or 1.0
|
||||
local timer_key = params.timer_key or "wait_timer"
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local elapsed = bb.floatValues[timer_key] or 0.0
|
||||
local dt = ecs.get_delta_time() or 0.016
|
||||
|
||||
elapsed = elapsed + dt
|
||||
bb.floatValues[timer_key] = elapsed
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
|
||||
if elapsed >= duration then
|
||||
bb.floatValues[timer_key] = nil
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
return "success"
|
||||
end
|
||||
|
||||
return "running"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 4: Node that modifies blackboard values
|
||||
-- =============================================================================
|
||||
-- Adds a configurable amount to a blackboard integer value.
|
||||
|
||||
ecs.behavior_tree.register_node("add_blackboard_value", function(entity_id, params)
|
||||
local key = params.key
|
||||
local amount = tonumber(params.amount) or 1
|
||||
|
||||
if not key then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
bb.values[key] = (bb.values[key] or 0) + amount
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
return "success"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 5: Node that sets a blackboard bit
|
||||
-- =============================================================================
|
||||
-- Sets or clears a named bit in the blackboard.
|
||||
|
||||
ecs.behavior_tree.register_node("set_blackboard_bit", function(entity_id, params)
|
||||
local bit_name = params.bit
|
||||
local value = params.value == "1" or params.value == "true"
|
||||
|
||||
if not bit_name then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
bb.bits[bit_name] = value
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
return "success"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 6: Node that checks a blackboard bit
|
||||
-- =============================================================================
|
||||
-- Checks if a named bit is set to a specific value.
|
||||
|
||||
ecs.behavior_tree.register_node("check_blackboard_bit", function(entity_id, params)
|
||||
local bit_name = params.bit
|
||||
local expected = params.value ~= "0" and params.value ~= "false"
|
||||
|
||||
if not bit_name then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local actual = bb.bits[bit_name] == true
|
||||
if actual == expected then
|
||||
return "success"
|
||||
else
|
||||
return "failure"
|
||||
end
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 7: Random chance node
|
||||
-- =============================================================================
|
||||
-- Succeeds with a configurable probability (0.0 to 1.0).
|
||||
|
||||
ecs.behavior_tree.register_node("random_chance", function(entity_id, params)
|
||||
local probability = tonumber(params.probability) or 0.5
|
||||
local roll = math.random()
|
||||
if roll < probability then
|
||||
return "success"
|
||||
else
|
||||
return "failure"
|
||||
end
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Using Built-in Node Types via create_node()
|
||||
-- =============================================================================
|
||||
-- ecs.behavior_tree.create_node(type, name, params) creates a node table
|
||||
-- for any built-in C++ node type. If the first argument matches a registered
|
||||
-- Lua handler name, it creates a luaTask node instead.
|
||||
--
|
||||
-- Built-in node types:
|
||||
-- Control: sequence, selector, invert
|
||||
-- Animation: setAnimationState, isAnimationEnded
|
||||
-- Blackboard: setBit, checkBit, setValue, checkValue, blackboardDump
|
||||
-- Timing: delay
|
||||
-- Movement: teleportToChild
|
||||
-- Physics: disablePhysics, enablePhysics
|
||||
-- Events: sendEvent
|
||||
-- Inventory: hasItem, hasItemByName, countItem, pickupItem, dropItem,
|
||||
-- useItem, addItemToInventory
|
||||
-- Debug: debugPrint
|
||||
-- Lua: luaTask (auto-detected when name matches a registered handler)
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 8: Action using built-in animation nodes
|
||||
-- =============================================================================
|
||||
-- Uses setAnimationState and isAnimationEnded to play an animation and
|
||||
-- wait for it to finish.
|
||||
|
||||
ecs.action_db.add_action("play_walk_animation", 1,
|
||||
{}, -- preconditions
|
||||
{}, -- effects
|
||||
{ -- behavior tree
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"),
|
||||
ecs.behavior_tree.create_node("isAnimationEnded", "locomotion"),
|
||||
ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 9: Action using built-in delay node
|
||||
-- =============================================================================
|
||||
-- Waits for a specified duration using the built-in delay node.
|
||||
|
||||
ecs.action_db.add_action("wait_and_greet", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("delay", "", "2.0"),
|
||||
ecs.behavior_tree.create_node("say_hello", "message=Waited 2 seconds!")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 10: Action using built-in blackboard nodes
|
||||
-- =============================================================================
|
||||
-- Sets a blackboard bit, checks it, and sets a value.
|
||||
|
||||
ecs.action_db.add_action("blackboard_demo", 1,
|
||||
{},
|
||||
{
|
||||
bits = { has_sword = true },
|
||||
values = { gold = 100 }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("setBit", "has_sword", "1"),
|
||||
ecs.behavior_tree.create_node("checkBit", "has_sword"),
|
||||
ecs.behavior_tree.create_node("setValue", "gold", "100"),
|
||||
ecs.behavior_tree.create_node("checkValue", "gold", ">= 50"),
|
||||
ecs.behavior_tree.create_node("blackboardDump", "After setup")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 11: Action using built-in sendEvent node
|
||||
-- =============================================================================
|
||||
-- Sends an event with parameters.
|
||||
|
||||
ecs.action_db.add_action("trigger_quest_event", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("sendEvent", "quest_started",
|
||||
"quest_id=the_ancient_sword,quest_giver=elder"),
|
||||
ecs.behavior_tree.create_node("debugPrint", "Quest event sent")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 12: Action using built-in physics nodes
|
||||
-- =============================================================================
|
||||
-- Disables physics, waits, then re-enables.
|
||||
|
||||
ecs.action_db.add_action("physics_control", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("disablePhysics"),
|
||||
ecs.behavior_tree.create_node("delay", "", "1.0"),
|
||||
ecs.behavior_tree.create_node("enablePhysics")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 13: Action using built-in inventory nodes
|
||||
-- =============================================================================
|
||||
-- Adds an item to inventory, checks for it, uses it, then drops it.
|
||||
|
||||
ecs.action_db.add_action("inventory_demo", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("addItemToInventory", "potion_01",
|
||||
"Health Potion,misc,1,0.5,10"),
|
||||
ecs.behavior_tree.create_node("hasItem", "potion_01"),
|
||||
ecs.behavior_tree.create_node("countItem", "potion_01", "1"),
|
||||
ecs.behavior_tree.create_node("useItem", "potion_01"),
|
||||
ecs.behavior_tree.create_node("dropItem", "potion_01", "1")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 14: Action using built-in teleport node
|
||||
-- =============================================================================
|
||||
-- Teleports the entity to a named child transform.
|
||||
|
||||
ecs.action_db.add_action("teleport_to_entrance", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("teleportToChild", "entrance"),
|
||||
ecs.behavior_tree.create_node("debugPrint", "Teleported to entrance")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 15: Complex action mixing Lua and built-in nodes
|
||||
-- =============================================================================
|
||||
-- A selector that first tries to use a sword, and if not available,
|
||||
-- picks one up and equips it.
|
||||
|
||||
ecs.action_db.add_action("equip_sword", 2,
|
||||
{},
|
||||
{
|
||||
bits = { has_sword = true }
|
||||
},
|
||||
{
|
||||
type = "selector",
|
||||
children = {
|
||||
-- Try to use existing sword
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("checkBit", "has_sword"),
|
||||
ecs.behavior_tree.create_node("debugPrint", "Already have a sword")
|
||||
}
|
||||
},
|
||||
-- Pick up and equip a sword
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"),
|
||||
ecs.behavior_tree.create_node("delay", "", "3.0"),
|
||||
ecs.behavior_tree.create_node("addItemToInventory", "sword_01",
|
||||
"Iron Sword,weapon,1,2.5,50"),
|
||||
ecs.behavior_tree.create_node("setBit", "has_sword", "1"),
|
||||
ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle"),
|
||||
ecs.behavior_tree.create_node("debugPrint", "Picked up the sword!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 16: Action with conditional logic using Lua nodes
|
||||
-- =============================================================================
|
||||
-- Uses Lua-registered nodes for blackboard checks and modifications.
|
||||
|
||||
ecs.action_db.add_action("gain_experience", 2,
|
||||
{
|
||||
values = { experience = 0 }
|
||||
},
|
||||
{
|
||||
values = { experience = 15 }
|
||||
},
|
||||
{
|
||||
type = "selector",
|
||||
children = {
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("check_blackboard_value",
|
||||
"key=experience,min=50"),
|
||||
ecs.behavior_tree.create_node("say_hello",
|
||||
"message=You are experienced!"),
|
||||
ecs.behavior_tree.create_node("add_blackboard_value",
|
||||
"key=experience,amount=10")
|
||||
}
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("say_hello",
|
||||
"message=You are still learning..."),
|
||||
ecs.behavior_tree.create_node("add_blackboard_value",
|
||||
"key=experience,amount=5")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 17: Action with random outcomes
|
||||
-- =============================================================================
|
||||
-- Uses the Lua random_chance node for probabilistic behavior.
|
||||
|
||||
ecs.action_db.add_action("try_gamble", 3,
|
||||
{},
|
||||
{
|
||||
values = { gold = 10 }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("random_chance", "probability=0.3"),
|
||||
ecs.behavior_tree.create_node("say_hello", "message=You won the gamble!"),
|
||||
ecs.behavior_tree.create_node("add_blackboard_value", "key=gold,amount=10")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 18: Action with running-state Lua node
|
||||
-- =============================================================================
|
||||
-- Uses the wait_for_duration Lua node which returns "running" each frame.
|
||||
|
||||
ecs.action_db.add_action("wait_and_continue", 1,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("wait_for_duration",
|
||||
"duration=2.5,timer_key=my_timer"),
|
||||
ecs.behavior_tree.create_node("say_hello", "message=Done waiting!")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 19: Action with blackboard bit operations via Lua nodes
|
||||
-- =============================================================================
|
||||
|
||||
ecs.action_db.add_action("toggle_flag", 1,
|
||||
{},
|
||||
{
|
||||
bits = { quest_complete = true }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("set_blackboard_bit",
|
||||
"bit=quest_complete,value=1"),
|
||||
ecs.behavior_tree.create_node("check_blackboard_bit",
|
||||
"bit=quest_complete,value=1"),
|
||||
ecs.behavior_tree.create_node("debugPrint", "Quest flag set and verified")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Managing Registered Nodes
|
||||
-- =============================================================================
|
||||
|
||||
-- List all registered node handlers:
|
||||
local nodes = ecs.behavior_tree.list_nodes()
|
||||
print("Registered behavior tree nodes:")
|
||||
for i, name in ipairs(nodes) do
|
||||
print(" " .. i .. ". " .. name)
|
||||
end
|
||||
|
||||
-- Unregister a node handler:
|
||||
-- local removed = ecs.behavior_tree.unregister_node("say_hello")
|
||||
-- if removed then
|
||||
-- print("Unregistered node: say_hello")
|
||||
-- end
|
||||
|
||||
-- =============================================================================
|
||||
-- Parameter Format Reference
|
||||
-- =============================================================================
|
||||
--
|
||||
-- The params string passed to create_node() supports:
|
||||
--
|
||||
-- Integer: "count=42" -> params.count = 42 (number)
|
||||
-- Float: "speed=3.5" -> params.speed = 3.5 (number)
|
||||
-- String: "msg=hello" -> params.msg = "hello" (string)
|
||||
-- Quoted: 'msg="hello world"' -> params.msg = "hello world" (string)
|
||||
-- Multiple: "key=val,count=5" -> params.key = "val", params.count = 5
|
||||
-- Empty: "" -> params = {} (empty table)
|
||||
--
|
||||
-- The params table is passed as the second argument to your registered
|
||||
-- Lua function handler.
|
||||
-- =============================================================================
|
||||
711
src/features/editScene/lua-examples/component_example.lua
Normal file
711
src/features/editScene/lua-examples/component_example.lua
Normal file
@@ -0,0 +1,711 @@
|
||||
-- =============================================================================
|
||||
-- Component Lua API Examples
|
||||
-- =============================================================================
|
||||
-- This file demonstrates how to add, remove, query, and manipulate ECS
|
||||
-- components from Lua using the ecs.* Lua API.
|
||||
--
|
||||
-- Components are data attached to entities. They define what an entity IS
|
||||
-- and what it CAN DO. For example, a Transform component gives an entity
|
||||
-- a position in the world, while a Renderable component makes it visible.
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Checking for Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create an entity and check what components it has:
|
||||
local entity = ecs.create_entity()
|
||||
ecs.set_entity_name(entity, "TestObject")
|
||||
|
||||
-- Check if an entity has a specific component:
|
||||
if ecs.has_component(entity, "Transform") then
|
||||
print("Entity has Transform component")
|
||||
end
|
||||
|
||||
-- New entities typically have EditorMarker by default:
|
||||
if ecs.has_component(entity, "EditorMarker") then
|
||||
print("Entity has EditorMarker (default for new entities)")
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Adding and Removing Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add a tag component (a component with no data):
|
||||
ecs.add_component(entity, "InWater")
|
||||
print("Added InWater tag")
|
||||
|
||||
-- Check it was added:
|
||||
if ecs.has_component(entity, "InWater") then
|
||||
print("Entity is now in water")
|
||||
end
|
||||
|
||||
-- Remove a tag component:
|
||||
ecs.remove_component(entity, "InWater")
|
||||
if not ecs.has_component(entity, "InWater") then
|
||||
print("Entity is no longer in water")
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Setting and Getting Components with Data
|
||||
-- =============================================================================
|
||||
|
||||
-- Set a component with data (creates or replaces it):
|
||||
ecs.set_component(entity, "Transform", {
|
||||
position = { 10, 20, 30 },
|
||||
rotation = { 1, 0, 0, 0 }, -- quaternion w, x, y, z
|
||||
scale = { 2, 2, 2 }
|
||||
})
|
||||
|
||||
-- Get the component back:
|
||||
local transform = ecs.get_component(entity, "Transform")
|
||||
if transform then
|
||||
print("Position: " .. transform.position[1] .. ", "
|
||||
.. transform.position[2] .. ", "
|
||||
.. transform.position[3])
|
||||
print("Scale: " .. transform.scale[1] .. ", "
|
||||
.. transform.scale[2] .. ", "
|
||||
.. transform.scale[3])
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Individual Field Access
|
||||
-- =============================================================================
|
||||
|
||||
-- Get a single field from a component:
|
||||
local pos_x = ecs.get_field(entity, "Transform", "position")
|
||||
print("Position X from get_field: " .. pos_x[1])
|
||||
|
||||
-- Set a single field:
|
||||
ecs.set_field(entity, "Transform", "position", { 0, 0, 0 })
|
||||
local new_pos = ecs.get_field(entity, "Transform", "position")
|
||||
print("New position: " .. new_pos[1] .. ", " .. new_pos[2] .. ", " .. new_pos[3])
|
||||
|
||||
-- =============================================================================
|
||||
-- Practical Examples: Common Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a player character:
|
||||
function create_player(name, x, y, z)
|
||||
local player = ecs.create_entity()
|
||||
ecs.set_entity_name(player, name)
|
||||
|
||||
-- Transform (position in the world):
|
||||
ecs.set_component(player, "Transform", {
|
||||
position = { x, y, z },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 1, 1, 1 }
|
||||
})
|
||||
|
||||
-- Renderable (visible mesh):
|
||||
ecs.set_component(player, "Renderable", {
|
||||
meshName = "character.mesh",
|
||||
visible = true
|
||||
})
|
||||
|
||||
-- Character (physics capsule for movement):
|
||||
ecs.set_component(player, "Character", {
|
||||
radius = 0.4,
|
||||
height = 1.8,
|
||||
offset = { 0, 0.9, 0 },
|
||||
enabled = true,
|
||||
useGravity = true
|
||||
})
|
||||
|
||||
-- PlayerController (camera and input):
|
||||
ecs.set_component(player, "PlayerController", {
|
||||
cameraMode = 1,
|
||||
tpsDistance = 5.0,
|
||||
tpsHeight = 2.0,
|
||||
mouseSensitivity = 0.5
|
||||
})
|
||||
|
||||
-- Inventory:
|
||||
ecs.set_component(player, "Inventory", {
|
||||
maxSlots = 20,
|
||||
maxWeight = 50.0,
|
||||
isContainer = true
|
||||
})
|
||||
|
||||
print("Created player: " .. name)
|
||||
return player
|
||||
end
|
||||
|
||||
local hero = create_player("Hero", 0, 0, 0)
|
||||
|
||||
-- Create a light source:
|
||||
function create_light(name, x, y, z, light_type)
|
||||
local light = ecs.create_entity()
|
||||
ecs.set_entity_name(light, name)
|
||||
|
||||
ecs.set_component(light, "Transform", {
|
||||
position = { x, y, z },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 1, 1, 1 }
|
||||
})
|
||||
|
||||
ecs.set_component(light, "Light", {
|
||||
lightType = light_type or 0, -- 0=point, 1=directional, 2=spot
|
||||
diffuseColor = { 1, 1, 1, 1 },
|
||||
intensity = 1.5,
|
||||
range = 100,
|
||||
castShadows = true
|
||||
})
|
||||
|
||||
print("Created light: " .. name)
|
||||
return light
|
||||
end
|
||||
|
||||
local sun = create_light("Sun", 0, 100, 0, 1) -- directional light
|
||||
|
||||
-- Create a building with smart object interaction:
|
||||
function create_building(name, x, z)
|
||||
local building = ecs.create_entity()
|
||||
ecs.set_entity_name(building, name)
|
||||
|
||||
ecs.set_component(building, "Transform", {
|
||||
position = { x, 0, z },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 1, 1, 1 }
|
||||
})
|
||||
|
||||
ecs.set_component(building, "Renderable", {
|
||||
meshName = "building.mesh",
|
||||
visible = true
|
||||
})
|
||||
|
||||
ecs.set_component(building, "SmartObject", {
|
||||
radius = 2.0,
|
||||
height = 3.0,
|
||||
actionNames = { "enter", "exit" }
|
||||
})
|
||||
|
||||
-- RigidBody for physics:
|
||||
ecs.set_component(building, "RigidBody", {
|
||||
bodyType = 0, -- 0=static
|
||||
mass = 0,
|
||||
friction = 0.5,
|
||||
restitution = 0.1,
|
||||
enabled = true
|
||||
})
|
||||
|
||||
print("Created building: " .. name)
|
||||
return building
|
||||
end
|
||||
|
||||
local house = create_building("House", 10, 10)
|
||||
|
||||
-- Create an item:
|
||||
function create_item(name, item_id, x, y, z)
|
||||
local item = ecs.create_entity()
|
||||
ecs.set_entity_name(item, name)
|
||||
|
||||
ecs.set_component(item, "Transform", {
|
||||
position = { x, y, z },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 0.5, 0.5, 0.5 }
|
||||
})
|
||||
|
||||
ecs.set_component(item, "Renderable", {
|
||||
meshName = "potion.mesh",
|
||||
visible = true
|
||||
})
|
||||
|
||||
ecs.set_component(item, "Item", {
|
||||
itemName = name,
|
||||
itemType = "consumable",
|
||||
itemId = item_id,
|
||||
stackSize = 1,
|
||||
maxStackSize = 10,
|
||||
weight = 0.5,
|
||||
value = 50,
|
||||
useActionName = "drink_potion"
|
||||
})
|
||||
|
||||
print("Created item: " .. name)
|
||||
return item
|
||||
end
|
||||
|
||||
local potion = create_item("Health Potion", "potion_health", 5, 0.5, 5)
|
||||
|
||||
-- Create a NavMesh area:
|
||||
function create_navmesh_area(name)
|
||||
local nav = ecs.create_entity()
|
||||
ecs.set_entity_name(nav, name)
|
||||
|
||||
ecs.set_component(nav, "NavMesh", {
|
||||
cellSize = 0.3,
|
||||
cellHeight = 0.2,
|
||||
agentHeight = 2.0,
|
||||
agentRadius = 0.5,
|
||||
agentMaxClimb = 0.5,
|
||||
agentMaxSlope = 45.0,
|
||||
enabled = true
|
||||
})
|
||||
|
||||
print("Created NavMesh: " .. name)
|
||||
return nav
|
||||
end
|
||||
|
||||
local navmesh = create_navmesh_area("MainNavMesh")
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Tag Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Tag components are boolean markers with no data fields.
|
||||
-- Common tags include:
|
||||
-- EditorMarker - marks entities visible in the editor
|
||||
-- InWater - entity is currently in water
|
||||
-- GeneratedPhysicsTag - physics was auto-generated
|
||||
-- ParentComponent - entity is a parent in a hierarchy
|
||||
-- NavMeshGeometrySource - entity contributes to navmesh generation
|
||||
|
||||
-- Example: Mark entities for different purposes:
|
||||
local marker1 = ecs.create_entity()
|
||||
ecs.add_component(marker1, "GeneratedPhysicsTag")
|
||||
|
||||
local marker2 = ecs.create_entity()
|
||||
ecs.add_component(marker2, "NavMeshGeometrySource")
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with GOAP Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create an NPC with GOAP AI:
|
||||
function create_npc(name, x, z)
|
||||
local npc = ecs.create_entity()
|
||||
ecs.set_entity_name(npc, name)
|
||||
|
||||
ecs.set_component(npc, "Transform", {
|
||||
position = { x, 0, z },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 1, 1, 1 }
|
||||
})
|
||||
|
||||
ecs.set_component(npc, "Character", {
|
||||
radius = 0.4,
|
||||
height = 1.8,
|
||||
offset = { 0, 0.9, 0 },
|
||||
enabled = true,
|
||||
useGravity = true
|
||||
})
|
||||
|
||||
-- GOAP blackboard (character's knowledge about the world):
|
||||
ecs.set_component(npc, "GoapBlackboard", {
|
||||
values = {
|
||||
health = 100,
|
||||
stamina = 100,
|
||||
hunger = 0,
|
||||
wood_count = 0
|
||||
},
|
||||
floatValues = {
|
||||
hunger = 0.0
|
||||
},
|
||||
stringValues = {
|
||||
state = "idle"
|
||||
}
|
||||
})
|
||||
|
||||
-- GOAP planner (plans actions to achieve goals):
|
||||
ecs.set_component(npc, "GoapPlanner", {
|
||||
enabled = true,
|
||||
maxIterations = 100
|
||||
})
|
||||
|
||||
-- GOAP runner (executes the plan):
|
||||
ecs.set_component(npc, "GoapRunner", {
|
||||
enabled = true,
|
||||
currentAction = "idle"
|
||||
})
|
||||
|
||||
-- Behavior tree for idle behavior:
|
||||
ecs.set_component(npc, "BehaviorTree", {
|
||||
treeName = "idle_behavior",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- Path following for movement:
|
||||
ecs.set_component(npc, "PathFollowing", {
|
||||
enabled = true,
|
||||
speed = 1.5
|
||||
})
|
||||
|
||||
-- NavMesh agent for navigation:
|
||||
ecs.set_component(npc, "NavMeshAgent", {
|
||||
enabled = true,
|
||||
radius = 0.5,
|
||||
height = 2.0
|
||||
})
|
||||
|
||||
print("Created NPC: " .. name)
|
||||
return npc
|
||||
end
|
||||
|
||||
local npc = create_npc("Villager_01", -10, -10)
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Event Handler Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add event handlers to entities:
|
||||
ecs.set_component(npc, "EventHandler", {
|
||||
eventName = "collision",
|
||||
actionName = "handle_collision",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Dialogue Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add dialogue to an NPC:
|
||||
ecs.set_component(npc, "Dialogue", {
|
||||
text = "Hello there, traveler!",
|
||||
speaker = "Villager_01",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Water Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a water plane:
|
||||
function create_water(name, y)
|
||||
local water = ecs.create_entity()
|
||||
ecs.set_entity_name(water, name)
|
||||
|
||||
ecs.set_component(water, "WaterPlane", {
|
||||
enabled = true,
|
||||
waterSurfaceY = y or 0.0,
|
||||
planeSize = 1000,
|
||||
reflectivity = 0.5,
|
||||
waveSpeed = 1.0
|
||||
})
|
||||
|
||||
ecs.set_component(water, "WaterPhysics", {
|
||||
enabled = true,
|
||||
waveHeight = 0.5
|
||||
})
|
||||
|
||||
print("Created water: " .. name)
|
||||
return water
|
||||
end
|
||||
|
||||
local ocean = create_water("Ocean", 0.0)
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Sky and Sun Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a skybox:
|
||||
function create_sky(name)
|
||||
local sky = ecs.create_entity()
|
||||
ecs.set_entity_name(sky, name)
|
||||
|
||||
ecs.set_component(sky, "Skybox", {
|
||||
enabled = true,
|
||||
size = 500,
|
||||
starsEnabled = true
|
||||
})
|
||||
|
||||
print("Created sky: " .. name)
|
||||
return sky
|
||||
end
|
||||
|
||||
local skybox = create_sky("Skybox")
|
||||
|
||||
-- Update sun properties:
|
||||
ecs.set_component(sun, "Sun", {
|
||||
enabled = true,
|
||||
timeOfDay = 12.0,
|
||||
timeSpeed = 1.0,
|
||||
intensity = 1.0,
|
||||
castShadows = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Camera Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a camera:
|
||||
function create_camera(name)
|
||||
local cam = ecs.create_entity()
|
||||
ecs.set_entity_name(cam, name)
|
||||
|
||||
ecs.set_component(cam, "Camera", {
|
||||
fovY = 60,
|
||||
nearClip = 0.1,
|
||||
farClip = 1000,
|
||||
orthographic = false
|
||||
})
|
||||
|
||||
print("Created camera: " .. name)
|
||||
return cam
|
||||
end
|
||||
|
||||
local camera = create_camera("MainCamera")
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Prefab Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Mark an entity as a prefab instance:
|
||||
ecs.set_component(house, "PrefabInstance", {
|
||||
prefabPath = "prefabs/house.json",
|
||||
instantiated = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with CellGrid Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a cell grid for spatial partitioning:
|
||||
function create_cell_grid(name, width, height, depth)
|
||||
local grid = ecs.create_entity()
|
||||
ecs.set_entity_name(grid, name)
|
||||
|
||||
ecs.set_component(grid, "CellGrid", {
|
||||
width = width or 10,
|
||||
height = height or 5,
|
||||
depth = depth or 10,
|
||||
cellSize = 1.0,
|
||||
cellHeight = 0.5
|
||||
})
|
||||
|
||||
print("Created cell grid: " .. name)
|
||||
return grid
|
||||
end
|
||||
|
||||
local grid = create_cell_grid("WorldGrid", 20, 10, 20)
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Town/District/Lot/Room Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a town with districts, lots, and rooms:
|
||||
function create_town(name)
|
||||
local town = ecs.create_entity()
|
||||
ecs.set_entity_name(town, name)
|
||||
|
||||
ecs.set_component(town, "Town", {
|
||||
townName = name,
|
||||
population = 500
|
||||
})
|
||||
|
||||
-- Create a district:
|
||||
local district = ecs.create_entity()
|
||||
ecs.set_entity_name(district, "Market District")
|
||||
ecs.set_component(district, "District", {
|
||||
districtName = "Market District",
|
||||
districtType = "commercial"
|
||||
})
|
||||
|
||||
-- Create a lot:
|
||||
local lot = ecs.create_entity()
|
||||
ecs.set_entity_name(lot, "Residential Lot 1")
|
||||
ecs.set_component(lot, "Lot", {
|
||||
lotName = "Residential Lot 1",
|
||||
lotType = "residential",
|
||||
width = 20,
|
||||
depth = 30
|
||||
})
|
||||
|
||||
-- Create a room:
|
||||
local room = ecs.create_entity()
|
||||
ecs.set_entity_name(room, "Kitchen")
|
||||
ecs.set_component(room, "Room", {
|
||||
roomName = "Kitchen",
|
||||
roomType = "kitchen",
|
||||
floor = 0
|
||||
})
|
||||
|
||||
print("Created town: " .. name)
|
||||
return town
|
||||
end
|
||||
|
||||
local rivendell = create_town("Rivendell")
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Furniture Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a furniture template:
|
||||
function create_furniture(name, mesh, category)
|
||||
local furniture = ecs.create_entity()
|
||||
ecs.set_entity_name(furniture, name)
|
||||
|
||||
ecs.set_component(furniture, "FurnitureTemplate", {
|
||||
templateName = name,
|
||||
meshName = mesh or "chair.mesh",
|
||||
category = category or "seating"
|
||||
})
|
||||
|
||||
print("Created furniture: " .. name)
|
||||
return furniture
|
||||
end
|
||||
|
||||
local chair = create_furniture("Wooden Chair", "chair.mesh", "seating")
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Animation Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add animation tree to a character:
|
||||
ecs.set_component(npc, "AnimationTree", {
|
||||
treeName = "humanoid",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
ecs.set_component(npc, "AnimationTreeTemplate", {
|
||||
templateName = "humanoid_base",
|
||||
blendTime = 0.2
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Physics Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add a physics collider to an entity:
|
||||
ecs.set_component(house, "PhysicsCollider", {
|
||||
shapeType = "box",
|
||||
size = { 5, 3, 5 },
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- Add buoyancy info for water physics:
|
||||
ecs.set_component(house, "BuoyancyInfo", {
|
||||
enabled = true,
|
||||
buoyancy = 1.0,
|
||||
linearDrag = 0.5,
|
||||
angularDrag = 0.3
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with LOD Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Add LOD settings:
|
||||
ecs.set_component(entity, "Lod", {
|
||||
lodLevel = 0,
|
||||
distance = 100.0
|
||||
})
|
||||
|
||||
ecs.set_component(entity, "LodSettings", {
|
||||
enabled = true,
|
||||
lodBias = 1.0
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Static Geometry Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Batch entities into static geometry:
|
||||
ecs.set_component(entity, "StaticGeometry", {
|
||||
batchName = "forest_batch",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
ecs.set_component(entity, "StaticGeometryMember", {
|
||||
parentBatch = "forest_batch",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Procedural Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create procedural textures and materials:
|
||||
ecs.set_component(entity, "ProceduralTexture", {
|
||||
textureName = "grass",
|
||||
width = 512,
|
||||
height = 512
|
||||
})
|
||||
|
||||
ecs.set_component(entity, "ProceduralMaterial", {
|
||||
materialName = "ground_mat",
|
||||
baseColor = { 0.5, 0.5, 0.5, 1.0 }
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Primitive Components
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a primitive shape:
|
||||
ecs.set_component(entity, "Primitive", {
|
||||
primitiveType = "box",
|
||||
size = { 1, 2, 1 }
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Triangle Buffer Components
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "TriangleBuffer", {
|
||||
enabled = true,
|
||||
vertexCount = 100
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Character Slots
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "CharacterSlots", {
|
||||
slotCount = 8
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Roof Components
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "Roof", {
|
||||
roofType = "gable",
|
||||
height = 2.5,
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with ClearArea Components
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "ClearArea", {
|
||||
radius = 5.0,
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Action Database Components
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "ActionDatabase", {
|
||||
enabled = true
|
||||
})
|
||||
|
||||
ecs.set_component(entity, "ActionDebug", {
|
||||
enabled = true,
|
||||
showDebugInfo = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Working with Startup Menu Components
|
||||
-- =============================================================================
|
||||
|
||||
ecs.set_component(entity, "StartupMenu", {
|
||||
enabled = true,
|
||||
showOnStart = true
|
||||
})
|
||||
|
||||
-- =============================================================================
|
||||
-- Component Lifecycle Summary
|
||||
-- =============================================================================
|
||||
|
||||
-- Components can be:
|
||||
-- 1. Added: ecs.add_component(entity, "ComponentName")
|
||||
-- 2. Removed: ecs.remove_component(entity, "ComponentName")
|
||||
-- 3. Checked: ecs.has_component(entity, "ComponentName")
|
||||
-- 4. Get: ecs.get_component(entity, "ComponentName")
|
||||
-- 5. Set: ecs.set_component(entity, "ComponentName", { fields... })
|
||||
-- 6. Field: ecs.get_field(entity, "ComponentName", "fieldName")
|
||||
-- 7. Field: ecs.set_field(entity, "ComponentName", "fieldName", value)
|
||||
|
||||
print("Component API examples completed successfully!")
|
||||
28
src/features/editScene/lua-examples/debug_crash_example.lua
Normal file
28
src/features/editScene/lua-examples/debug_crash_example.lua
Normal 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)")
|
||||
102
src/features/editScene/lua-examples/dialogue_basic_show.lua
Normal file
102
src/features/editScene/lua-examples/dialogue_basic_show.lua
Normal 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!")
|
||||
219
src/features/editScene/lua-examples/dialogue_component_api.lua
Normal file
219
src/features/editScene/lua-examples/dialogue_component_api.lua
Normal 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!")
|
||||
260
src/features/editScene/lua-examples/dialogue_event_handler.lua
Normal file
260
src/features/editScene/lua-examples/dialogue_event_handler.lua
Normal 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!")
|
||||
143
src/features/editScene/lua-examples/dialogue_event_subscribe.lua
Normal file
143
src/features/editScene/lua-examples/dialogue_event_subscribe.lua
Normal 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!")
|
||||
320
src/features/editScene/lua-examples/dialogue_sequence.lua
Normal file
320
src/features/editScene/lua-examples/dialogue_sequence.lua
Normal 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!")
|
||||
177
src/features/editScene/lua-examples/entity_example.lua
Normal file
177
src/features/editScene/lua-examples/entity_example.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
-- =============================================================================
|
||||
-- Entity Lua API Examples
|
||||
-- =============================================================================
|
||||
-- This file demonstrates how to create, manage, and query entities from Lua
|
||||
-- using the ecs.* Lua API.
|
||||
--
|
||||
-- Entities are the fundamental building blocks of the ECS world. Every object
|
||||
-- in the game world is an entity with a unique numeric ID.
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Creating Entities
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a new entity. Returns a numeric ID.
|
||||
local player = ecs.create_entity()
|
||||
print("Created entity with ID: " .. player)
|
||||
|
||||
-- Create multiple entities:
|
||||
local npc1 = ecs.create_entity()
|
||||
local npc2 = ecs.create_entity()
|
||||
local item = ecs.create_entity()
|
||||
|
||||
-- =============================================================================
|
||||
-- Entity Existence and Lifecycle
|
||||
-- =============================================================================
|
||||
|
||||
-- Check if an entity exists:
|
||||
if ecs.entity_exists(player) then
|
||||
print("Player entity exists")
|
||||
end
|
||||
|
||||
-- Check a non-existent entity:
|
||||
if not ecs.entity_exists(999999) then
|
||||
print("Entity 999999 does not exist")
|
||||
end
|
||||
|
||||
-- Destroy an entity:
|
||||
ecs.destroy_entity(npc2)
|
||||
if not ecs.entity_exists(npc2) then
|
||||
print("npc2 was destroyed")
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Entity Names
|
||||
-- =============================================================================
|
||||
|
||||
-- Set an entity's name:
|
||||
ecs.set_entity_name(player, "Hero")
|
||||
ecs.set_entity_name(npc1, "Villager_01")
|
||||
ecs.set_entity_name(item, "Health_Potion")
|
||||
|
||||
-- Get an entity's name:
|
||||
local name = ecs.get_entity_name(player)
|
||||
print("Player name: " .. name)
|
||||
|
||||
-- Look up an entity by name:
|
||||
local found = ecs.get_entity_by_name("Villager_01")
|
||||
if found then
|
||||
print("Found entity " .. found .. " with name 'Villager_01'")
|
||||
end
|
||||
|
||||
-- Look up a non-existent name:
|
||||
local not_found = ecs.get_entity_by_name("Nonexistent")
|
||||
if not_found == nil then
|
||||
print("'Nonexistent' not found (returns nil)")
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Entity Hierarchy (Parent/Children)
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a parent-child hierarchy:
|
||||
local house = ecs.create_entity()
|
||||
ecs.set_entity_name(house, "House")
|
||||
|
||||
local door = ecs.create_entity()
|
||||
ecs.set_entity_name(door, "Door")
|
||||
|
||||
local window = ecs.create_entity()
|
||||
ecs.set_entity_name(window, "Window")
|
||||
|
||||
-- Check parent of a child (initially none):
|
||||
local p = ecs.parent(door)
|
||||
if p == nil then
|
||||
print("Door has no parent initially")
|
||||
end
|
||||
|
||||
-- Check children of a parent (initially none):
|
||||
local kids = ecs.children(house)
|
||||
print("House has " .. #kids .. " children initially")
|
||||
|
||||
-- =============================================================================
|
||||
-- Practical Example: Creating a Scene
|
||||
-- =============================================================================
|
||||
|
||||
-- Create a complete scene with entities:
|
||||
function create_scene()
|
||||
-- Create terrain
|
||||
local terrain = ecs.create_entity()
|
||||
ecs.set_entity_name(terrain, "Terrain")
|
||||
|
||||
-- Create lighting
|
||||
local sun = ecs.create_entity()
|
||||
ecs.set_entity_name(sun, "Sun")
|
||||
|
||||
-- Create buildings
|
||||
local buildings = {}
|
||||
for i = 1, 3 do
|
||||
local bldg = ecs.create_entity()
|
||||
ecs.set_entity_name(bldg, "Building_" .. i)
|
||||
table.insert(buildings, bldg)
|
||||
end
|
||||
|
||||
-- Create characters
|
||||
local characters = {}
|
||||
for i = 1, 5 do
|
||||
local char = ecs.create_entity()
|
||||
ecs.set_entity_name(char, "Character_" .. i)
|
||||
table.insert(characters, char)
|
||||
end
|
||||
|
||||
print("Scene created with:")
|
||||
print(" 1 terrain entity")
|
||||
print(" 1 sun entity")
|
||||
print(" " .. #buildings .. " buildings")
|
||||
print(" " .. #characters .. " characters")
|
||||
|
||||
return {
|
||||
terrain = terrain,
|
||||
sun = sun,
|
||||
buildings = buildings,
|
||||
characters = characters
|
||||
}
|
||||
end
|
||||
|
||||
local scene = create_scene()
|
||||
|
||||
-- =============================================================================
|
||||
-- Entity ID Properties
|
||||
-- =============================================================================
|
||||
|
||||
-- Entity IDs are positive integers:
|
||||
local e = ecs.create_entity()
|
||||
assert(type(e) == "number", "Entity ID should be a number")
|
||||
assert(e > 0, "Entity ID should be positive")
|
||||
|
||||
-- Each entity gets a unique ID:
|
||||
local ids = {}
|
||||
for i = 1, 10 do
|
||||
ids[i] = ecs.create_entity()
|
||||
end
|
||||
for i = 1, 10 do
|
||||
for j = i + 1, 10 do
|
||||
assert(ids[i] ~= ids[j], "IDs should be unique")
|
||||
end
|
||||
end
|
||||
print("All 10 entities have unique IDs")
|
||||
|
||||
-- Destroyed entity IDs are not reused immediately:
|
||||
local old_id = ecs.create_entity()
|
||||
ecs.destroy_entity(old_id)
|
||||
local new_id = ecs.create_entity()
|
||||
assert(new_id ~= old_id, "New entity should have different ID")
|
||||
print("Destroyed entity ID " .. old_id .. " is not reused")
|
||||
|
||||
-- =============================================================================
|
||||
-- Error Handling
|
||||
-- =============================================================================
|
||||
|
||||
-- These operations should not crash:
|
||||
ecs.destroy_entity(999999) -- destroying non-existent entity is safe
|
||||
ecs.set_entity_name(999999, "ghost") -- setting name on non-existent entity
|
||||
local n = ecs.get_entity_name(999999) -- returns empty string
|
||||
print("Name of non-existent entity: '" .. n .. "'")
|
||||
|
||||
print("Entity API examples completed successfully!")
|
||||
296
src/features/editScene/lua-examples/event_example.lua
Normal file
296
src/features/editScene/lua-examples/event_example.lua
Normal 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!")
|
||||
135
src/features/editScene/lua-examples/game_mode_example.lua
Normal file
135
src/features/editScene/lua-examples/game_mode_example.lua
Normal 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!")
|
||||
576
src/features/editScene/lua/LuaActionApi.cpp
Normal file
576
src/features/editScene/lua/LuaActionApi.cpp
Normal file
@@ -0,0 +1,576 @@
|
||||
#include "LuaActionApi.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push a BehaviorTreeNode as a Lua table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void pushBehaviorTree(lua_State *L, const BehaviorTreeNode &node)
|
||||
{
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushstring(L, node.type.c_str());
|
||||
lua_setfield(L, -2, "type");
|
||||
|
||||
if (!node.name.empty()) {
|
||||
lua_pushstring(L, node.name.c_str());
|
||||
lua_setfield(L, -2, "name");
|
||||
}
|
||||
|
||||
if (!node.params.empty()) {
|
||||
lua_pushstring(L, node.params.c_str());
|
||||
lua_setfield(L, -2, "params");
|
||||
}
|
||||
|
||||
if (!node.children.empty()) {
|
||||
lua_newtable(L);
|
||||
for (size_t i = 0; i < node.children.size(); i++) {
|
||||
pushBehaviorTree(L, node.children[i]);
|
||||
lua_rawseti(L, -2, (int)i + 1);
|
||||
}
|
||||
lua_setfield(L, -2, "children");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: read a BehaviorTreeNode from a Lua table at given index
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static BehaviorTreeNode readBehaviorTree(lua_State *L, int idx)
|
||||
{
|
||||
BehaviorTreeNode node;
|
||||
|
||||
if (!lua_istable(L, idx))
|
||||
return node;
|
||||
|
||||
// type (required)
|
||||
lua_getfield(L, idx, "type");
|
||||
if (lua_isstring(L, -1))
|
||||
node.type = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// name (optional)
|
||||
lua_getfield(L, idx, "name");
|
||||
if (lua_isstring(L, -1))
|
||||
node.name = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// params (optional)
|
||||
lua_getfield(L, idx, "params");
|
||||
if (lua_isstring(L, -1))
|
||||
node.params = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// children (optional array)
|
||||
lua_getfield(L, idx, "children");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
// key is numeric index, value is child table
|
||||
if (lua_istable(L, -1)) {
|
||||
node.children.push_back(
|
||||
readBehaviorTree(L, lua_gettop(L)));
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push a GoapBlackboard as a Lua table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void pushBlackboard(lua_State *L, const GoapBlackboard &bb)
|
||||
{
|
||||
lua_newtable(L); // blackboard table
|
||||
|
||||
// Bits
|
||||
lua_newtable(L); // bits table
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (bb.hasBit(i)) {
|
||||
const char *name = GoapBlackboard::getBitName(i);
|
||||
const char *key = name ? name : "";
|
||||
lua_pushboolean(L, bb.getBit(i));
|
||||
lua_setfield(L, -2, key);
|
||||
}
|
||||
}
|
||||
lua_setfield(L, -2, "bits");
|
||||
|
||||
// Integer values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.values) {
|
||||
lua_pushinteger(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "values");
|
||||
|
||||
// Float values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.floatValues) {
|
||||
lua_pushnumber(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "floatValues");
|
||||
|
||||
// String values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.stringValues) {
|
||||
lua_pushstring(L, kv.second.c_str());
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "stringValues");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: read a GoapBlackboard from a Lua table (optional, at given index)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static GoapBlackboard readBlackboard(lua_State *L, int idx)
|
||||
{
|
||||
GoapBlackboard bb;
|
||||
|
||||
if (!lua_istable(L, idx))
|
||||
return bb;
|
||||
|
||||
// Read bits
|
||||
lua_getfield(L, idx, "bits");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
// key is the bit name (string), value is boolean
|
||||
if (lua_isstring(L, -2) && lua_isboolean(L, -1)) {
|
||||
const char *name = lua_tostring(L, -2);
|
||||
bool val = lua_toboolean(L, -1) != 0;
|
||||
// Find bit index by name (auto-registers if new)
|
||||
int idx2 = GoapBlackboard::findBitByName(name);
|
||||
if (idx2 < 0) {
|
||||
// Find first free slot
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (GoapBlackboard::getBitName(
|
||||
i) == nullptr) {
|
||||
GoapBlackboard::setBitName(
|
||||
i, name);
|
||||
idx2 = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (idx2 >= 0)
|
||||
bb.setBit(idx2, val);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Read integer values
|
||||
lua_getfield(L, idx, "values");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isinteger(L, -1))
|
||||
bb.values[lua_tostring(L, -2)] =
|
||||
(int)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Read float values
|
||||
lua_getfield(L, idx, "floatValues");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isnumber(L, -1))
|
||||
bb.floatValues[lua_tostring(L, -2)] =
|
||||
(float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Read string values
|
||||
lua_getfield(L, idx, "stringValues");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isstring(L, -1))
|
||||
bb.stringValues[lua_tostring(L, -2)] =
|
||||
lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
return bb;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.add_action(name, cost, preconds, effects, behaviorTree)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaAddAction(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
int cost = (int)luaL_optinteger(L, 2, 1);
|
||||
|
||||
GoapAction action(name, cost);
|
||||
|
||||
// Optional preconditions table (arg 3)
|
||||
if (lua_gettop(L) >= 3 && lua_istable(L, 3))
|
||||
action.preconditions = readBlackboard(L, 3);
|
||||
|
||||
// Optional effects table (arg 4)
|
||||
if (lua_gettop(L) >= 4 && lua_istable(L, 4))
|
||||
action.effects = readBlackboard(L, 4);
|
||||
|
||||
// Optional behavior tree table (arg 5)
|
||||
if (lua_gettop(L) >= 5 && lua_istable(L, 5))
|
||||
action.behaviorTree = readBehaviorTree(L, 5);
|
||||
|
||||
ActionDatabase::getSingleton().addOrReplaceAction(action);
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua] Added action: " << name;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.add_goal(name, priority, target, condition)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaAddGoal(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
int priority = (int)luaL_optinteger(L, 2, 1);
|
||||
|
||||
GoapGoal goal(name, priority);
|
||||
|
||||
// Optional target blackboard (arg 3)
|
||||
if (lua_gettop(L) >= 3 && lua_istable(L, 3))
|
||||
goal.target = readBlackboard(L, 3);
|
||||
|
||||
// Optional condition string (arg 4)
|
||||
if (lua_gettop(L) >= 4 && lua_isstring(L, 4))
|
||||
goal.condition = lua_tostring(L, 4);
|
||||
|
||||
ActionDatabase::getSingleton().addOrReplaceGoal(goal);
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua] Added goal: " << name;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.remove_action(name) -> bool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaRemoveAction(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
bool removed = ActionDatabase::getSingleton().removeAction(name);
|
||||
lua_pushboolean(L, removed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.remove_goal(name) -> bool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaRemoveGoal(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
bool removed = ActionDatabase::getSingleton().removeGoal(name);
|
||||
lua_pushboolean(L, removed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.find_action(name) -> table or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaFindAction(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
const GoapAction *action =
|
||||
ActionDatabase::getSingleton().findAction(name);
|
||||
|
||||
if (!action) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_newtable(L);
|
||||
lua_pushstring(L, action->name.c_str());
|
||||
lua_setfield(L, -2, "name");
|
||||
lua_pushinteger(L, action->cost);
|
||||
lua_setfield(L, -2, "cost");
|
||||
|
||||
pushBlackboard(L, action->preconditions);
|
||||
lua_setfield(L, -2, "preconditions");
|
||||
|
||||
pushBlackboard(L, action->effects);
|
||||
lua_setfield(L, -2, "effects");
|
||||
|
||||
// Behavior tree
|
||||
pushBehaviorTree(L, action->behaviorTree);
|
||||
lua_setfield(L, -2, "behaviorTree");
|
||||
|
||||
// Behavior tree name (optional reference)
|
||||
if (!action->behaviorTreeName.empty()) {
|
||||
lua_pushstring(L, action->behaviorTreeName.c_str());
|
||||
lua_setfield(L, -2, "behaviorTreeName");
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.find_goal(name) -> table or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaFindGoal(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
const GoapGoal *goal = ActionDatabase::getSingleton().findGoal(name);
|
||||
|
||||
if (!goal) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_newtable(L);
|
||||
lua_pushstring(L, goal->name.c_str());
|
||||
lua_setfield(L, -2, "name");
|
||||
lua_pushinteger(L, goal->priority);
|
||||
lua_setfield(L, -2, "priority");
|
||||
|
||||
pushBlackboard(L, goal->target);
|
||||
lua_setfield(L, -2, "target");
|
||||
|
||||
lua_pushstring(L, goal->condition.c_str());
|
||||
lua_setfield(L, -2, "condition");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.list_actions() -> table of action names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaListActions(lua_State *L)
|
||||
{
|
||||
const auto &db = ActionDatabase::getSingleton();
|
||||
lua_newtable(L);
|
||||
int idx = 1;
|
||||
for (const auto &action : db.actions) {
|
||||
lua_pushstring(L, action.name.c_str());
|
||||
lua_rawseti(L, -2, idx++);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.list_goals() -> table of goal names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaListGoals(lua_State *L)
|
||||
{
|
||||
const auto &db = ActionDatabase::getSingleton();
|
||||
lua_newtable(L);
|
||||
int idx = 1;
|
||||
for (const auto &goal : db.goals) {
|
||||
lua_pushstring(L, goal.name.c_str());
|
||||
lua_rawseti(L, -2, idx++);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.clear() -> nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaClear(lua_State *L)
|
||||
{
|
||||
(void)L;
|
||||
ActionDatabase::getSingleton().clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.set_bit_name(index, name) -> nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaSetBitName(lua_State *L)
|
||||
{
|
||||
int index = (int)luaL_checkinteger(L, 1);
|
||||
const char *name = luaL_checkstring(L, 2);
|
||||
|
||||
if (index < 0 || index >= 64)
|
||||
luaL_error(L, "bit index must be 0-63, got %d", index);
|
||||
|
||||
GoapBlackboard::setBitName(index, name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.find_bit_by_name(name) -> index or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaFindBitByName(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
int index = GoapBlackboard::findBitByName(name);
|
||||
if (index < 0) {
|
||||
lua_pushnil(L);
|
||||
} else {
|
||||
lua_pushinteger(L, index);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.get_bit_name(index) -> name or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetBitName(lua_State *L)
|
||||
{
|
||||
int index = (int)luaL_checkinteger(L, 1);
|
||||
if (index < 0 || index >= 64) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
const char *name = GoapBlackboard::getBitName(index);
|
||||
if (name) {
|
||||
lua_pushstring(L, name);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.list_bit_names() -> table of { index, name }
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaListBitNames(lua_State *L)
|
||||
{
|
||||
lua_newtable(L);
|
||||
int idx = 1;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
const char *name = GoapBlackboard::getBitName(i);
|
||||
if (name) {
|
||||
lua_newtable(L);
|
||||
lua_pushinteger(L, i);
|
||||
lua_setfield(L, -2, "index");
|
||||
lua_pushstring(L, name);
|
||||
lua_setfield(L, -2, "name");
|
||||
lua_rawseti(L, -2, idx++);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.action_db.auto_assign_bit(name) -> index
|
||||
// ---------------------------------------------------------------------------
|
||||
// Finds a bit by name, or auto-assigns the first free slot if not found.
|
||||
// Returns the bit index, or -1 if all 64 slots are full.
|
||||
|
||||
static int luaAutoAssignBit(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
int index = GoapBlackboard::findBitByName(name);
|
||||
if (index >= 0) {
|
||||
lua_pushinteger(L, index);
|
||||
return 1;
|
||||
}
|
||||
// Find first free slot
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (GoapBlackboard::getBitName(i) == nullptr) {
|
||||
GoapBlackboard::setBitName(i, name);
|
||||
lua_pushinteger(L, i);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
lua_pushinteger(L, -1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaActionApi(lua_State *L)
|
||||
{
|
||||
// Get or create the "ecs" global table
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
}
|
||||
|
||||
// Create the action_db sub-table
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushcfunction(L, luaAddAction);
|
||||
lua_setfield(L, -2, "add_action");
|
||||
|
||||
lua_pushcfunction(L, luaAddGoal);
|
||||
lua_setfield(L, -2, "add_goal");
|
||||
|
||||
lua_pushcfunction(L, luaRemoveAction);
|
||||
lua_setfield(L, -2, "remove_action");
|
||||
|
||||
lua_pushcfunction(L, luaRemoveGoal);
|
||||
lua_setfield(L, -2, "remove_goal");
|
||||
|
||||
lua_pushcfunction(L, luaFindAction);
|
||||
lua_setfield(L, -2, "find_action");
|
||||
|
||||
lua_pushcfunction(L, luaFindGoal);
|
||||
lua_setfield(L, -2, "find_goal");
|
||||
|
||||
lua_pushcfunction(L, luaListActions);
|
||||
lua_setfield(L, -2, "list_actions");
|
||||
|
||||
lua_pushcfunction(L, luaListGoals);
|
||||
lua_setfield(L, -2, "list_goals");
|
||||
|
||||
lua_pushcfunction(L, luaClear);
|
||||
lua_setfield(L, -2, "clear");
|
||||
|
||||
// Bit name management
|
||||
lua_pushcfunction(L, luaSetBitName);
|
||||
lua_setfield(L, -2, "set_bit_name");
|
||||
|
||||
lua_pushcfunction(L, luaFindBitByName);
|
||||
lua_setfield(L, -2, "find_bit_by_name");
|
||||
|
||||
lua_pushcfunction(L, luaGetBitName);
|
||||
lua_setfield(L, -2, "get_bit_name");
|
||||
|
||||
lua_pushcfunction(L, luaListBitNames);
|
||||
lua_setfield(L, -2, "list_bit_names");
|
||||
|
||||
lua_pushcfunction(L, luaAutoAssignBit);
|
||||
lua_setfield(L, -2, "auto_assign_bit");
|
||||
|
||||
// Set action_db as a field of ecs
|
||||
lua_setfield(L, -2, "action_db");
|
||||
|
||||
// Ensure ecs is global
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
101
src/features/editScene/lua/LuaActionApi.hpp
Normal file
101
src/features/editScene/lua/LuaActionApi.hpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#ifndef EDITSCENE_LUA_ACTION_API_HPP
|
||||
#define EDITSCENE_LUA_ACTION_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
/**
|
||||
* @file LuaActionApi.hpp
|
||||
* @brief Lua API for the ActionDatabase singleton.
|
||||
*
|
||||
* Provides Lua functions to create, query, and manage GOAP actions
|
||||
* and goals in the global ActionDatabase singleton.
|
||||
*
|
||||
* Exposed Lua globals (under the "ecs" table):
|
||||
* ecs.action_db.add_action(name, cost, preconds, effects, behaviorTree) -> nil
|
||||
* ecs.action_db.add_goal(name, priority, target, condition) -> nil
|
||||
* ecs.action_db.remove_action(name) -> bool
|
||||
* ecs.action_db.remove_goal(name) -> bool
|
||||
* ecs.action_db.find_action(name) -> table or nil
|
||||
* ecs.action_db.find_goal(name) -> table or nil
|
||||
* ecs.action_db.list_actions() -> table of action names
|
||||
* ecs.action_db.list_goals() -> table of goal names
|
||||
* ecs.action_db.clear() -> nil
|
||||
*
|
||||
* Bit Name Management:
|
||||
* ecs.action_db.set_bit_name(index, name) -> nil
|
||||
* Assign a human-readable name to a bit slot (0-63).
|
||||
* Example: ecs.action_db.set_bit_name(0, "has_axe")
|
||||
*
|
||||
* ecs.action_db.find_bit_by_name(name) -> index or nil
|
||||
* Look up which bit index a name is assigned to.
|
||||
* Returns nil if the name hasn't been assigned yet.
|
||||
*
|
||||
* ecs.action_db.get_bit_name(index) -> name or nil
|
||||
* Get the name assigned to a bit slot, or nil if unassigned.
|
||||
*
|
||||
* ecs.action_db.list_bit_names() -> table of { index, name }
|
||||
* Returns an array of all assigned bit names with their indices.
|
||||
*
|
||||
* ecs.action_db.auto_assign_bit(name) -> index
|
||||
* Find a bit by name, or auto-assign the first free slot.
|
||||
* Returns the bit index, or -1 if all 64 slots are full.
|
||||
* This is the same logic used internally by readBlackboard()
|
||||
* when it encounters an unknown bit name in preconditions/effects.
|
||||
*
|
||||
* Bit Name Convention:
|
||||
* Bit names are global across the entire game session. They map
|
||||
* human-readable names (like "has_axe", "is_hungry") to bit indices
|
||||
* (0-63) used in GoapBlackboard preconditions and effects.
|
||||
*
|
||||
* You can define bits explicitly at startup:
|
||||
* ecs.action_db.set_bit_name(0, "has_axe")
|
||||
* ecs.action_db.set_bit_name(1, "has_wood")
|
||||
* ecs.action_db.set_bit_name(2, "is_hungry")
|
||||
*
|
||||
* Or let them be auto-assigned when you use them in actions:
|
||||
* ecs.action_db.add_action("chop_wood", 2,
|
||||
* { bits = { has_axe = true } }, -- "has_axe" auto-assigned if new
|
||||
* { bits = { has_wood = true } })
|
||||
*
|
||||
* Use auto_assign_bit() to explicitly reserve a name:
|
||||
* local idx = ecs.action_db.auto_assign_bit("my_flag")
|
||||
* -- idx is now the bit index for "my_flag"
|
||||
*
|
||||
* Use list_bit_names() to see all currently assigned names:
|
||||
* local bits = ecs.action_db.list_bit_names()
|
||||
* for _, b in ipairs(bits) do
|
||||
* print(b.index .. ": " .. b.name)
|
||||
* end
|
||||
*
|
||||
* Behavior Tree Table Format (arg 5 of add_action):
|
||||
* {
|
||||
* type = "sequence", -- node type: sequence, selector, invert, task, check, etc.
|
||||
* name = "optional_name", -- action/condition name, animation state, etc.
|
||||
* params = "optional_params", -- extra parameters (e.g. delay seconds, bit index)
|
||||
* children = { -- array of child nodes (for sequence/selector/invert)
|
||||
* { type = "task", name = "myAction" },
|
||||
* { type = "setAnimationState", name = "SM/Walk" },
|
||||
* { type = "delay", params = "2.0" }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* See BehaviorTree.hpp for full list of node types.
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register all ActionDatabase-related Lua API functions.
|
||||
*
|
||||
* Adds action_db sub-table to the "ecs" global table.
|
||||
* Must be called after LuaState is constructed.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaActionApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_ACTION_API_HPP
|
||||
471
src/features/editScene/lua/LuaBehaviorTreeApi.cpp
Normal file
471
src/features/editScene/lua/LuaBehaviorTreeApi.cpp
Normal file
@@ -0,0 +1,471 @@
|
||||
#include "LuaBehaviorTreeApi.hpp"
|
||||
#include "LuaEntityApi.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global registry of Lua node handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
// Maps node name -> Lua registry reference for the callback function.
|
||||
// The callback is stored as a Lua reference in the registry so it persists
|
||||
// across Lua state resets and can be called from C++.
|
||||
|
||||
static std::unordered_map<std::string, int> g_luaNodeHandlers;
|
||||
|
||||
// Global Lua state pointer, set during registerLuaBehaviorTreeApi().
|
||||
// Used by callLuaBehaviorTreeNode() which is invoked from the C++
|
||||
// behavior tree system without direct access to the Lua state.
|
||||
static lua_State *g_luaState = nullptr;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push a GoapBlackboard as a Lua table (reused from LuaActionApi)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void pushBlackboard(lua_State *L, const GoapBlackboard &bb)
|
||||
{
|
||||
lua_newtable(L); // blackboard table
|
||||
|
||||
// Bits
|
||||
lua_newtable(L); // bits table
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (bb.hasBit(i)) {
|
||||
const char *name = GoapBlackboard::getBitName(i);
|
||||
const char *key = name ? name : "";
|
||||
lua_pushboolean(L, bb.getBit(i));
|
||||
lua_setfield(L, -2, key);
|
||||
}
|
||||
}
|
||||
lua_setfield(L, -2, "bits");
|
||||
|
||||
// Integer values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.values) {
|
||||
lua_pushinteger(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "values");
|
||||
|
||||
// Float values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.floatValues) {
|
||||
lua_pushnumber(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "floatValues");
|
||||
|
||||
// String values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.stringValues) {
|
||||
lua_pushstring(L, kv.second.c_str());
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "stringValues");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: parse "key=val,key2=val2" params into a Lua table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void pushParamsTable(lua_State *L, const std::string ¶ms)
|
||||
{
|
||||
lua_newtable(L); // params table
|
||||
|
||||
if (params.empty())
|
||||
return;
|
||||
|
||||
const char *s = params.c_str();
|
||||
while (*s) {
|
||||
// Skip whitespace
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
if (!*s)
|
||||
break;
|
||||
|
||||
// Find key
|
||||
const char *keyStart = s;
|
||||
while (*s && *s != '=' && *s != ',')
|
||||
s++;
|
||||
std::string key(keyStart, static_cast<size_t>(s - keyStart));
|
||||
// Trim trailing spaces from key
|
||||
while (!key.empty() &&
|
||||
(key.back() == ' ' || key.back() == '\t'))
|
||||
key.pop_back();
|
||||
|
||||
if (*s != '=') {
|
||||
while (*s && *s != ',')
|
||||
s++;
|
||||
if (*s == ',')
|
||||
s++;
|
||||
continue;
|
||||
}
|
||||
s++; // skip '='
|
||||
|
||||
// Skip whitespace before value
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
|
||||
// Find value end (next comma or end)
|
||||
const char *valStart = s;
|
||||
bool inQuotes = false;
|
||||
while (*s && (*s != ',' || inQuotes)) {
|
||||
if (*s == '"')
|
||||
inQuotes = !inQuotes;
|
||||
s++;
|
||||
}
|
||||
std::string val(valStart, static_cast<size_t>(s - valStart));
|
||||
// Trim trailing spaces from value
|
||||
while (!val.empty() &&
|
||||
(val.back() == ' ' || val.back() == '\t'))
|
||||
val.pop_back();
|
||||
|
||||
// Strip quotes if present
|
||||
if (val.size() >= 2 && val.front() == '"' &&
|
||||
val.back() == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
lua_pushstring(L, val.c_str());
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Try numeric
|
||||
char *end = nullptr;
|
||||
long iVal = strtol(val.c_str(), &end, 10);
|
||||
if (end != val.c_str() && *end == '\0') {
|
||||
lua_pushinteger(L, (int)iVal);
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Try float
|
||||
end = nullptr;
|
||||
float fVal = strtof(val.c_str(), &end);
|
||||
if (end != val.c_str() && *end == '\0') {
|
||||
lua_pushnumber(L, fVal);
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Fallback to string
|
||||
lua_pushstring(L, val.c_str());
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (*s == ',')
|
||||
s++;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API: Call a registered Lua node handler
|
||||
// ---------------------------------------------------------------------------
|
||||
// Returns: 0 = success, 1 = failure, 2 = running, -1 = not found/error
|
||||
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
flecs::entity entity, const std::string ¶ms)
|
||||
{
|
||||
// Use the global Lua state if none was passed
|
||||
lua_State *L = static_cast<lua_State *>(L_);
|
||||
if (!L)
|
||||
L = g_luaState;
|
||||
if (!L)
|
||||
return -1;
|
||||
|
||||
auto it = g_luaNodeHandlers.find(nodeName);
|
||||
if (it == g_luaNodeHandlers.end())
|
||||
return -1;
|
||||
|
||||
int ref = it->second;
|
||||
|
||||
// Push the callback function
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||
|
||||
// Push entity ID as first argument
|
||||
lua_pushinteger(L, luaEntityToId(entity));
|
||||
|
||||
// Push params table as second argument
|
||||
pushParamsTable(L, params);
|
||||
|
||||
// Call the function
|
||||
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Error calling node '" << nodeName
|
||||
<< "': " << lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read the result
|
||||
if (!lua_isstring(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *result = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (strcmp(result, "success") == 0)
|
||||
return 0;
|
||||
if (strcmp(result, "failure") == 0)
|
||||
return 1;
|
||||
if (strcmp(result, "running") == 0)
|
||||
return 2;
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Node '" << nodeName
|
||||
<< "' returned invalid result: " << result;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Convenience overload: call with raw entity ID (for tests)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
uint64_t entityId, const std::string ¶ms)
|
||||
{
|
||||
// Use the global Lua state if none was passed
|
||||
lua_State *L = static_cast<lua_State *>(L_);
|
||||
if (!L)
|
||||
L = g_luaState;
|
||||
if (!L)
|
||||
return -1;
|
||||
|
||||
auto it = g_luaNodeHandlers.find(nodeName);
|
||||
if (it == g_luaNodeHandlers.end())
|
||||
return -1;
|
||||
|
||||
int ref = it->second;
|
||||
|
||||
// Push the callback function
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||
|
||||
// Push entity ID as first argument
|
||||
lua_pushinteger(L, (lua_Integer)entityId);
|
||||
|
||||
// Push params table as second argument
|
||||
pushParamsTable(L, params);
|
||||
|
||||
// Call the function
|
||||
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Error calling node '" << nodeName
|
||||
<< "': " << lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read the result
|
||||
if (!lua_isstring(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *result = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (strcmp(result, "success") == 0)
|
||||
return 0;
|
||||
if (strcmp(result, "failure") == 0)
|
||||
return 1;
|
||||
if (strcmp(result, "running") == 0)
|
||||
return 2;
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Node '" << nodeName
|
||||
<< "' returned invalid result: " << result;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.register_node(name, function)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaRegisterNode(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
|
||||
if (!lua_isfunction(L, 2))
|
||||
luaL_error(L, "Expected function as second argument");
|
||||
|
||||
// Remove any existing handler with the same name
|
||||
auto it = g_luaNodeHandlers.find(name);
|
||||
if (it != g_luaNodeHandlers.end()) {
|
||||
luaL_unref(L, LUA_REGISTRYINDEX, it->second);
|
||||
}
|
||||
|
||||
// Store the function reference
|
||||
lua_pushvalue(L, 2);
|
||||
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
g_luaNodeHandlers[name] = ref;
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Registered node handler: " << name;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.unregister_node(name) -> bool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaUnregisterNode(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
|
||||
auto it = g_luaNodeHandlers.find(name);
|
||||
if (it == g_luaNodeHandlers.end()) {
|
||||
lua_pushboolean(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
luaL_unref(L, LUA_REGISTRYINDEX, it->second);
|
||||
g_luaNodeHandlers.erase(it);
|
||||
|
||||
lua_pushboolean(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.list_nodes() -> table of names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaListNodeHandlers(lua_State *L)
|
||||
{
|
||||
lua_newtable(L);
|
||||
int idx = 1;
|
||||
for (const auto &kv : g_luaNodeHandlers) {
|
||||
lua_pushstring(L, kv.first.c_str());
|
||||
lua_rawseti(L, -2, idx++);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.create_node(type, name, params) -> table
|
||||
// ecs.behavior_tree.create_node(type, params) -> table
|
||||
// ecs.behavior_tree.create_node(luaHandlerName, params) -> table (backward compat)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Creates a behavior tree node table suitable for use in action behavior trees.
|
||||
//
|
||||
// If the first argument matches a registered Lua node handler name, it creates
|
||||
// a luaTask node (backward compatible behavior).
|
||||
// Otherwise, the first argument is treated as the node type string.
|
||||
//
|
||||
// Examples:
|
||||
// ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk")
|
||||
// -> { type = "setAnimationState", name = "locomotion/walk" }
|
||||
//
|
||||
// ecs.behavior_tree.create_node("delay", "2.0")
|
||||
// -> { type = "delay", params = "2.0" }
|
||||
//
|
||||
// ecs.behavior_tree.create_node("delay", "", "2.0")
|
||||
// -> { type = "delay", params = "2.0" }
|
||||
//
|
||||
// ecs.behavior_tree.create_node("say_hello", "message=Hi")
|
||||
// -> { type = "luaTask", name = "say_hello", params = "message=Hi" }
|
||||
|
||||
static int luaCreateNode(lua_State *L)
|
||||
{
|
||||
const char *arg1 = luaL_checkstring(L, 1);
|
||||
|
||||
// Check if arg1 is a registered Lua node handler name (backward compat)
|
||||
bool isLuaHandler = g_luaNodeHandlers.find(arg1) !=
|
||||
g_luaNodeHandlers.end();
|
||||
|
||||
// Save arg2 and arg3 before pushing the result table (which shifts indices)
|
||||
const char *arg2 = NULL;
|
||||
const char *arg3 = NULL;
|
||||
if (lua_gettop(L) >= 2 && lua_isstring(L, 2))
|
||||
arg2 = lua_tostring(L, 2);
|
||||
if (lua_gettop(L) >= 3 && lua_isstring(L, 3))
|
||||
arg3 = lua_tostring(L, 3);
|
||||
|
||||
lua_newtable(L);
|
||||
|
||||
if (isLuaHandler) {
|
||||
// Backward compatible: create a luaTask node
|
||||
lua_pushstring(L, "luaTask");
|
||||
lua_setfield(L, -2, "type");
|
||||
|
||||
lua_pushstring(L, arg1);
|
||||
lua_setfield(L, -2, "name");
|
||||
|
||||
if (arg2 && arg2[0]) {
|
||||
lua_pushstring(L, arg2);
|
||||
lua_setfield(L, -2, "params");
|
||||
}
|
||||
} else {
|
||||
// arg1 is the node type
|
||||
lua_pushstring(L, arg1);
|
||||
lua_setfield(L, -2, "type");
|
||||
|
||||
// arg2 is either name or params depending on the node type
|
||||
if (arg2 && arg2[0]) {
|
||||
lua_pushstring(L, arg2);
|
||||
lua_setfield(L, -2, "name");
|
||||
}
|
||||
|
||||
// arg3 is params (optional)
|
||||
if (arg3 && arg3[0]) {
|
||||
lua_pushstring(L, arg3);
|
||||
lua_setfield(L, -2, "params");
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public C++ API: get list of registered Lua node names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::vector<std::string> getRegisteredLuaNodeNames()
|
||||
{
|
||||
std::vector<std::string> names;
|
||||
names.reserve(g_luaNodeHandlers.size());
|
||||
for (const auto &kv : g_luaNodeHandlers)
|
||||
names.push_back(kv.first);
|
||||
return names;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaBehaviorTreeApi(lua_State *L)
|
||||
{
|
||||
// Store the Lua state globally so callLuaBehaviorTreeNode can use it
|
||||
g_luaState = L;
|
||||
|
||||
// Get or create the "ecs" global table
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
}
|
||||
|
||||
// Create the behavior_tree sub-table
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushcfunction(L, luaRegisterNode);
|
||||
lua_setfield(L, -2, "register_node");
|
||||
|
||||
lua_pushcfunction(L, luaUnregisterNode);
|
||||
lua_setfield(L, -2, "unregister_node");
|
||||
|
||||
lua_pushcfunction(L, luaListNodeHandlers);
|
||||
lua_setfield(L, -2, "list_nodes");
|
||||
|
||||
lua_pushcfunction(L, luaCreateNode);
|
||||
lua_setfield(L, -2, "create_node");
|
||||
|
||||
// Set behavior_tree as a field of ecs
|
||||
lua_setfield(L, -2, "behavior_tree");
|
||||
|
||||
// Ensure ecs is global
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
149
src/features/editScene/lua/LuaBehaviorTreeApi.hpp
Normal file
149
src/features/editScene/lua/LuaBehaviorTreeApi.hpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#ifndef EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
|
||||
#define EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <flecs.h>
|
||||
|
||||
/**
|
||||
* @file LuaBehaviorTreeApi.hpp
|
||||
* @brief Lua API for creating behavior tree nodes that run Lua functions.
|
||||
*
|
||||
* Provides Lua bindings to register named Lua functions as behavior tree
|
||||
* node handlers, and to create behavior tree nodes that invoke those
|
||||
* functions during tree evaluation.
|
||||
*
|
||||
* Exposed Lua globals (under the "ecs" table):
|
||||
* ecs.behavior_tree.register_node(name, function) -> nil
|
||||
* Register a Lua function as a behavior tree node handler.
|
||||
* The function receives (entity_id, params_table) and must return
|
||||
* "success", "failure", or "running".
|
||||
*
|
||||
* ecs.behavior_tree.unregister_node(name) -> bool
|
||||
* Remove a previously registered node handler.
|
||||
*
|
||||
* ecs.behavior_tree.list_nodes() -> table of registered node names
|
||||
* Returns an array of all registered node handler names.
|
||||
*
|
||||
* ecs.behavior_tree.create_node(type, name, params) -> table
|
||||
* Creates a behavior tree node table suitable for use in
|
||||
* ecs.action_db.add_action() behavior trees.
|
||||
*
|
||||
* If the first argument matches a registered Lua node handler name,
|
||||
* it creates a luaTask node (backward compatible).
|
||||
* Otherwise, the first argument is the node type string.
|
||||
*
|
||||
* Examples:
|
||||
* ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk")
|
||||
* -> { type = "setAnimationState", name = "locomotion/walk" }
|
||||
*
|
||||
* ecs.behavior_tree.create_node("delay", "", "2.0")
|
||||
* -> { type = "delay", params = "2.0" }
|
||||
*
|
||||
* ecs.behavior_tree.create_node("say_hello", "message=Hi")
|
||||
* -> { type = "luaTask", name = "say_hello", params = "message=Hi" }
|
||||
*
|
||||
* Supported node types (see BehaviorTree.hpp for full list):
|
||||
* sequence, selector, invert, task, check, debugPrint,
|
||||
* setAnimationState, isAnimationEnded, setBit, checkBit,
|
||||
* setValue, checkValue, blackboardDump, delay, teleportToChild,
|
||||
* disablePhysics, enablePhysics, sendEvent, hasItem, hasItemByName,
|
||||
* countItem, pickupItem, dropItem, useItem, addItemToInventory,
|
||||
* luaTask
|
||||
*
|
||||
* Example:
|
||||
* -- Register a Lua node handler
|
||||
* ecs.behavior_tree.register_node("say_hello", function(entity_id, params)
|
||||
* print("Hello from entity " .. entity_id .. "! " .. (params.message or ""))
|
||||
* return "success"
|
||||
* end)
|
||||
*
|
||||
* -- Build a behavior tree using both built-in C++ nodes and Lua nodes
|
||||
* ecs.action_db.add_action("greet", 1, {}, {}, {
|
||||
* type = "sequence",
|
||||
* children = {
|
||||
* ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"),
|
||||
* ecs.behavior_tree.create_node("delay", "", "2.0"),
|
||||
* ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle"),
|
||||
* ecs.behavior_tree.create_node("say_hello", "message=Hello World")
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* -- A Lua node that runs for a while:
|
||||
* ecs.behavior_tree.register_node("wait_random", function(entity_id, params)
|
||||
* local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
* if not bb then return "failure" end
|
||||
*
|
||||
* local key = "wait_timer_" .. params.timer_key
|
||||
* local dt = ecs.get_delta_time()
|
||||
* bb.floatValues[key] = (bb.floatValues[key] or 0) + dt
|
||||
*
|
||||
* local duration = tonumber(params.duration) or 1.0
|
||||
* if bb.floatValues[key] >= duration then
|
||||
* bb.floatValues[key] = nil
|
||||
* ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
* return "success"
|
||||
* end
|
||||
* ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
* return "running"
|
||||
* end)
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register all behavior tree Lua API functions into the "ecs" table.
|
||||
*
|
||||
* Adds a behavior_tree sub-table to the "ecs" global table with functions
|
||||
* for registering Lua node handlers and creating behavior tree nodes.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaBehaviorTreeApi(lua_State *L);
|
||||
|
||||
/**
|
||||
* @brief Get the list of registered Lua node handler names.
|
||||
*
|
||||
* Used by the behavior tree editor UI to show registered Lua nodes
|
||||
* in a dropdown instead of requiring manual entry.
|
||||
*
|
||||
* @return A vector of registered node handler names.
|
||||
*/
|
||||
std::vector<std::string> getRegisteredLuaNodeNames();
|
||||
|
||||
/**
|
||||
* @brief Call a registered Lua behavior tree node handler.
|
||||
*
|
||||
* This is the primary evaluation function used by the behavior tree system.
|
||||
* It looks up the registered handler by name, pushes entity and params
|
||||
* as arguments, calls the Lua function, and interprets the result.
|
||||
*
|
||||
* @param L_ Pointer to the Lua state (can be nullptr to use global state).
|
||||
* @param nodeName The name of the registered node handler.
|
||||
* @param entity The flecs entity executing this node.
|
||||
* @param params The params string (key=val,key2=val2 format).
|
||||
* @return 0 = success, 1 = failure, 2 = running, -1 = error/not found.
|
||||
*/
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
flecs::entity entity, const std::string ¶ms);
|
||||
|
||||
/**
|
||||
* @brief Convenience overload for calling a registered Lua node handler
|
||||
* with a raw entity ID (for use in tests or contexts without flecs).
|
||||
*
|
||||
* @param L_ Pointer to the Lua state (can be nullptr to use global state).
|
||||
* @param nodeName The name of the registered node handler.
|
||||
* @param entityId The raw entity ID.
|
||||
* @param params The params string (key=val,key2=val2 format).
|
||||
* @return 0 = success, 1 = failure, 2 = running, -1 = error/not found.
|
||||
*/
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
uint64_t entityId, const std::string ¶ms);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
|
||||
1992
src/features/editScene/lua/LuaComponentApi.cpp
Normal file
1992
src/features/editScene/lua/LuaComponentApi.cpp
Normal file
File diff suppressed because it is too large
Load Diff
57
src/features/editScene/lua/LuaComponentApi.hpp
Normal file
57
src/features/editScene/lua/LuaComponentApi.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#ifndef EDITSCENE_LUA_COMPONENT_API_HPP
|
||||
#define EDITSCENE_LUA_COMPONENT_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
/**
|
||||
* @file LuaComponentApi.hpp
|
||||
* @brief Lua API for adding, removing, and modifying ECS components.
|
||||
*
|
||||
* Provides a generic component API where Lua scripts can add, get, set,
|
||||
* and remove components on entities. Each component type is registered
|
||||
* with a name string and a set of field accessors.
|
||||
*
|
||||
* Exposed Lua globals (in the "ecs" table):
|
||||
* ecs.add_component(id, "ComponentName") -> nil
|
||||
* ecs.remove_component(id, "ComponentName") -> nil
|
||||
* ecs.has_component(id, "ComponentName") -> bool
|
||||
* ecs.get_component(id, "ComponentName") -> table (field -> value)
|
||||
* ecs.set_component(id, "ComponentName", table) -> nil
|
||||
* ecs.get_field(id, "ComponentName", "fieldName") -> value
|
||||
* ecs.set_field(id, "ComponentName", "fieldName", value) -> nil
|
||||
*
|
||||
* Supported component names (case-sensitive):
|
||||
* "EntityName", "Transform", "Renderable", "Light", "Camera",
|
||||
* "RigidBody", "PhysicsCollider", "Character", "CharacterSlots",
|
||||
* "AnimationTree", "AnimationTreeTemplate", "BehaviorTree",
|
||||
* "GoapBlackboard", "GoapAction", "GoapGoal", "ActionDatabase",
|
||||
* "ActionDebug", "SmartObject", "Actuator", "EventHandler",
|
||||
* "GoapPlanner", "GoapRunner", "PathFollowing", "NavMesh",
|
||||
* "NavMeshGeometrySource", "NavMeshAgent", "Item", "Inventory",
|
||||
* "Lod", "LodSettings", "StaticGeometry", "StaticGeometryMember",
|
||||
* "ProceduralTexture", "ProceduralMaterial", "Primitive",
|
||||
* "TriangleBuffer", "Sun", "Skybox", "WaterPlane", "WaterPhysics",
|
||||
* "BuoyancyInfo", "InWater", "StartupMenu", "Dialogue",
|
||||
* "PlayerController", "CellGrid", "Room", "ClearArea", "Roof",
|
||||
* "Lot", "District", "Town", "FurnitureTemplate", "PrefabInstance",
|
||||
* "EditorMarker", "GeneratedPhysicsTag", "ParentComponent",
|
||||
* "ModifiedComponent"
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register all component Lua API functions into the "ecs" global table.
|
||||
*
|
||||
* Adds functions for component manipulation (add, remove, has, get, set,
|
||||
* get_field, set_field) to the existing "ecs" table.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaComponentApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_COMPONENT_API_HPP
|
||||
236
src/features/editScene/lua/LuaEntityApi.cpp
Normal file
236
src/features/editScene/lua/LuaEntityApi.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#include "LuaEntityApi.hpp"
|
||||
#include "components/EditorMarker.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <iostream>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// Global entity ID map
|
||||
LuaEntityIdMap g_luaEntityIdMap;
|
||||
|
||||
int luaEntityToId(flecs::entity e)
|
||||
{
|
||||
if (!e.is_valid())
|
||||
return -1;
|
||||
return g_luaEntityIdMap.addEntity(e);
|
||||
}
|
||||
|
||||
flecs::entity luaIdToEntity(int id)
|
||||
{
|
||||
return g_luaEntityIdMap.getEntity(id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: get the Flecs world from the Lua registry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static flecs::world getWorld(lua_State *L)
|
||||
{
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld");
|
||||
OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered");
|
||||
flecs::world *world =
|
||||
static_cast<flecs::world *>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return *world;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.create_entity() -> int (entity ID)
|
||||
// Creates a new entity with EditorMarkerComponent and returns its Lua ID.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaCreateEntity(lua_State *L)
|
||||
{
|
||||
flecs::world world = getWorld(L);
|
||||
flecs::entity e = world.entity();
|
||||
// Add EditorMarkerComponent so it appears in the editor hierarchy
|
||||
// (the component is forward-declared; we use a tag approach)
|
||||
e.add<EditorMarkerComponent>();
|
||||
int id = g_luaEntityIdMap.addEntity(e);
|
||||
lua_pushinteger(L, id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.destroy_entity(id) -> nil
|
||||
// Destroys an entity and removes it from the ID map.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaDestroyEntity(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
if (e.is_alive()) {
|
||||
g_luaEntityIdMap.removeEntity(e);
|
||||
e.destruct();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.entity_exists(id) -> bool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaEntityExists(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
lua_pushboolean(L, g_luaEntityIdMap.hasId(id) ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.get_player_entity() -> int (entity ID) or nil
|
||||
// Looks up the entity named "player" in the Flecs world.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetPlayerEntity(lua_State *L)
|
||||
{
|
||||
flecs::world world = getWorld(L);
|
||||
flecs::entity e = world.lookup("player");
|
||||
if (e.is_valid()) {
|
||||
int id = g_luaEntityIdMap.addEntity(e);
|
||||
lua_pushinteger(L, id);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.get_entity_by_name(name) -> int (entity ID) or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetEntityByName(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TSTRING);
|
||||
const char *name = lua_tostring(L, 1);
|
||||
flecs::world world = getWorld(L);
|
||||
flecs::entity e = world.lookup(name);
|
||||
if (e.is_valid()) {
|
||||
int id = g_luaEntityIdMap.addEntity(e);
|
||||
lua_pushinteger(L, id);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.set_entity_name(id, name) -> nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaSetEntityName(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
luaL_checktype(L, 2, LUA_TSTRING);
|
||||
int id = lua_tointeger(L, 1);
|
||||
const char *name = lua_tostring(L, 2);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
e.set_name(name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.get_entity_name(id) -> string or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetEntityName(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
const char *name = e.name();
|
||||
if (name && name[0]) {
|
||||
lua_pushstring(L, name);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.parent(id) -> int (parent ID) or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetParent(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
flecs::entity parent = e.parent();
|
||||
if (parent.is_valid()) {
|
||||
int parentId = g_luaEntityIdMap.addEntity(parent);
|
||||
lua_pushinteger(L, parentId);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.children(id) -> table of child IDs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetChildren(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
|
||||
lua_newtable(L); // result table
|
||||
int index = 1;
|
||||
e.children([&](flecs::entity child) {
|
||||
int childId = g_luaEntityIdMap.addEntity(child);
|
||||
lua_pushinteger(L, childId);
|
||||
lua_rawseti(L, -2, index);
|
||||
index++;
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register all entity API functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaEntityApi(lua_State *L)
|
||||
{
|
||||
// Create the "ecs" global table
|
||||
lua_newtable(L);
|
||||
|
||||
// Entity management
|
||||
lua_pushcfunction(L, luaCreateEntity);
|
||||
lua_setfield(L, -2, "create_entity");
|
||||
|
||||
lua_pushcfunction(L, luaDestroyEntity);
|
||||
lua_setfield(L, -2, "destroy_entity");
|
||||
|
||||
lua_pushcfunction(L, luaEntityExists);
|
||||
lua_setfield(L, -2, "entity_exists");
|
||||
|
||||
lua_pushcfunction(L, luaGetPlayerEntity);
|
||||
lua_setfield(L, -2, "get_player_entity");
|
||||
|
||||
lua_pushcfunction(L, luaGetEntityByName);
|
||||
lua_setfield(L, -2, "get_entity_by_name");
|
||||
|
||||
lua_pushcfunction(L, luaSetEntityName);
|
||||
lua_setfield(L, -2, "set_entity_name");
|
||||
|
||||
lua_pushcfunction(L, luaGetEntityName);
|
||||
lua_setfield(L, -2, "get_entity_name");
|
||||
|
||||
// Hierarchy
|
||||
lua_pushcfunction(L, luaGetParent);
|
||||
lua_setfield(L, -2, "parent");
|
||||
|
||||
lua_pushcfunction(L, luaGetChildren);
|
||||
lua_setfield(L, -2, "children");
|
||||
|
||||
// Set the global
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
126
src/features/editScene/lua/LuaEntityApi.hpp
Normal file
126
src/features/editScene/lua/LuaEntityApi.hpp
Normal file
@@ -0,0 +1,126 @@
|
||||
#ifndef EDITSCENE_LUA_ENTITY_API_HPP
|
||||
#define EDITSCENE_LUA_ENTITY_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <lua.hpp>
|
||||
#include <Ogre.h>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* @file LuaEntityApi.hpp
|
||||
* @brief Lua API for entity creation, destruction, and ID mapping.
|
||||
*
|
||||
* Provides a bidirectional mapping between Lua integer IDs and
|
||||
* Flecs entity handles. Lua scripts reference entities by integer
|
||||
* IDs rather than raw Flecs handles for safety and simplicity.
|
||||
*
|
||||
* Exposed Lua globals:
|
||||
* ecs.create_entity() -> int (entity ID)
|
||||
* ecs.destroy_entity(id) -> nil
|
||||
* ecs.entity_exists(id) -> bool
|
||||
* ecs.get_player_entity() -> int (entity ID)
|
||||
* ecs.get_entity_by_name(name) -> int (entity ID) or nil
|
||||
* ecs.set_entity_name(id, name)-> nil
|
||||
* ecs.get_entity_name(id) -> string or nil
|
||||
* ecs.parent(id) -> int (parent ID) or nil
|
||||
* ecs.children(id) -> table of child IDs
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Global bidirectional mapping between Lua integer IDs and Flecs entities.
|
||||
*
|
||||
* This is a singleton-like global so that all Lua API functions can
|
||||
* access it without needing to pass it through every closure.
|
||||
*/
|
||||
struct LuaEntityIdMap {
|
||||
std::unordered_map<int, flecs::entity> id2entity;
|
||||
std::unordered_map<flecs::entity_t, int> entity2id;
|
||||
int nextId = 0;
|
||||
|
||||
/** @brief Get the next available integer ID. */
|
||||
int getNextId()
|
||||
{
|
||||
nextId++;
|
||||
return nextId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add an entity to the map, returning its integer ID.
|
||||
* If the entity is already mapped, returns the existing ID.
|
||||
*/
|
||||
int addEntity(flecs::entity e)
|
||||
{
|
||||
if (entity2id.find(e.id()) != entity2id.end())
|
||||
return entity2id[e.id()];
|
||||
int id = getNextId();
|
||||
id2entity[id] = e;
|
||||
entity2id[e.id()] = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the Flecs entity for an integer ID.
|
||||
* Asserts if the ID is not found or the entity is invalid.
|
||||
*/
|
||||
flecs::entity getEntity(int id)
|
||||
{
|
||||
auto it = id2entity.find(id);
|
||||
OgreAssert(it != id2entity.end(), "Invalid entity ID");
|
||||
OgreAssert(it->second.is_valid(), "Entity is no longer valid");
|
||||
return it->second;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Remove an entity from the map.
|
||||
*/
|
||||
void removeEntity(flecs::entity e)
|
||||
{
|
||||
auto it = entity2id.find(e.id());
|
||||
if (it != entity2id.end()) {
|
||||
id2entity.erase(it->second);
|
||||
entity2id.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if an integer ID is valid.
|
||||
*/
|
||||
bool hasId(int id) const
|
||||
{
|
||||
auto it = id2entity.find(id);
|
||||
return it != id2entity.end() && it->second.is_valid();
|
||||
}
|
||||
};
|
||||
|
||||
/** @brief Global entity ID map instance. */
|
||||
extern LuaEntityIdMap g_luaEntityIdMap;
|
||||
|
||||
/**
|
||||
* @brief Convert a Flecs entity to a Lua integer ID.
|
||||
* @return The integer ID, or -1 if the entity is invalid.
|
||||
*/
|
||||
int luaEntityToId(flecs::entity e);
|
||||
|
||||
/**
|
||||
* @brief Convert a Lua integer ID to a Flecs entity.
|
||||
* Asserts if the ID is invalid.
|
||||
*/
|
||||
flecs::entity luaIdToEntity(int id);
|
||||
|
||||
/**
|
||||
* @brief Register all entity-related Lua API functions into the global table.
|
||||
*
|
||||
* Creates the "ecs" global table (or adds to it) with entity management
|
||||
* functions. Must be called after LuaState is constructed.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaEntityApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_ENTITY_API_HPP
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user