Compare commits
93 Commits
d4061386ec
...
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 | |||
| 64b03abb48 | |||
| cfd9dde5da | |||
| 07101fcc64 | |||
| b8c61da1f7 | |||
| b1413d6d00 | |||
| f785339852 | |||
| c2cbd0974d | |||
| 9e72a48457 | |||
| ebd875feac | |||
| 2a2fd53c4f |
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,161 @@
|
||||
project(editScene)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
find_package(OGRE REQUIRED COMPONENTS Bites Overlay CONFIG)
|
||||
find_package(OGRE REQUIRED COMPONENTS Bites Overlay MeshLodGenerator CONFIG)
|
||||
find_package(flecs REQUIRED CONFIG)
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
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
|
||||
systems/StaticGeometrySystem.cpp
|
||||
systems/ProceduralTextureSystem.cpp
|
||||
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
|
||||
ui/RigidBodyEditor.cpp
|
||||
ui/LightEditor.cpp
|
||||
ui/CameraEditor.cpp
|
||||
ui/LodEditor.cpp
|
||||
ui/LodSettingsEditor.cpp
|
||||
ui/StaticGeometryEditor.cpp
|
||||
ui/StaticGeometryMemberEditor.cpp
|
||||
ui/ProceduralTextureEditor.cpp
|
||||
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
|
||||
components/StaticGeometryModule.cpp
|
||||
components/ProceduralTextureModule.cpp
|
||||
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
|
||||
@@ -31,18 +166,148 @@ 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
|
||||
components/LodSettings.hpp
|
||||
components/StaticGeometry.hpp
|
||||
components/StaticGeometryMember.hpp
|
||||
components/ProceduralTexture.hpp
|
||||
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
|
||||
ui/ComponentEditor.hpp
|
||||
ui/ComponentRegistry.hpp
|
||||
ui/ComponentRegistration.hpp
|
||||
ui/TransformEditor.hpp
|
||||
ui/RenderableEditor.hpp
|
||||
ui/PhysicsColliderEditor.hpp
|
||||
ui/RigidBodyEditor.hpp
|
||||
ui/LightEditor.hpp
|
||||
ui/CameraEditor.hpp
|
||||
ui/LodEditor.hpp
|
||||
ui/LodSettingsEditor.hpp
|
||||
ui/StaticGeometryEditor.hpp
|
||||
ui/StaticGeometryMemberEditor.hpp
|
||||
ui/ProceduralTextureEditor.hpp
|
||||
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})
|
||||
@@ -54,13 +319,159 @@ target_link_libraries(editSceneEditor
|
||||
OgreMain
|
||||
OgreBites
|
||||
OgreOverlay
|
||||
OgreMeshLodGenerator
|
||||
flecs::flecs_static
|
||||
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.)
|
||||
@@ -74,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,11 +9,69 @@
|
||||
#include <OgreRenderTargetListener.h>
|
||||
#include <flecs.h>
|
||||
#include <memory>
|
||||
#include "lua/LuaState.hpp"
|
||||
|
||||
// Forward declarations
|
||||
class EditorUISystem;
|
||||
class EditorCamera;
|
||||
class EditorPhysicsSystem;
|
||||
class EditorLightSystem;
|
||||
class EditorCameraSystem;
|
||||
class EditorLodSystem;
|
||||
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
|
||||
@@ -22,28 +80,46 @@ class EditorPhysicsSystem;
|
||||
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;
|
||||
@@ -61,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
|
||||
@@ -80,9 +222,52 @@ 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;
|
||||
std::unique_ptr<StaticGeometrySystem> m_staticGeometrySystem;
|
||||
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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
37
src/features/editScene/components/Camera.hpp
Normal file
37
src/features/editScene/components/Camera.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef EDITSCENE_CAMERA_HPP
|
||||
#define EDITSCENE_CAMERA_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Camera component - attaches an Ogre::Camera to the entity's SceneNode
|
||||
*/
|
||||
struct CameraComponent {
|
||||
// Camera properties
|
||||
float fovY = 45.0f; // Vertical field of view in degrees
|
||||
float nearClip = 0.1f; // Near clip distance
|
||||
float farClip = 1000.0f; // Far clip distance
|
||||
float aspectRatio = 16.0f / 9.0f; // Aspect ratio (width/height)
|
||||
|
||||
// For orthographic camera
|
||||
bool orthographic = false;
|
||||
float orthoWidth = 10.0f; // Width for orthographic view
|
||||
float orthoHeight = 10.0f; // Height for orthographic view
|
||||
|
||||
// The Ogre camera object (created by CameraSystem)
|
||||
Ogre::Camera* camera = nullptr;
|
||||
|
||||
// For preview (optional RTT - created on demand)
|
||||
Ogre::RenderTarget* previewTarget = nullptr;
|
||||
Ogre::TexturePtr previewTexture;
|
||||
bool showPreview = false;
|
||||
int previewWidth = 400;
|
||||
int previewHeight = 300;
|
||||
|
||||
void markDirty() { needsRebuild = true; }
|
||||
bool needsRebuild = true;
|
||||
bool needsPreviewUpdate = false;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CAMERA_HPP
|
||||
53
src/features/editScene/components/CameraModule.cpp
Normal file
53
src/features/editScene/components/CameraModule.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "Camera.hpp"
|
||||
#include "Transform.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/CameraEditor.hpp"
|
||||
#include "../systems/CameraSystem.hpp"
|
||||
|
||||
// Register Camera component
|
||||
REGISTER_COMPONENT("Camera", CameraComponent, CameraEditor)
|
||||
{
|
||||
registry.registerComponent<CameraComponent>(
|
||||
"Camera",
|
||||
std::make_unique<CameraEditor>(sceneMgr, renderWindow),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<CameraComponent>()) {
|
||||
// Camera requires Transform
|
||||
if (!e.has<TransformComponent>()) {
|
||||
// Auto-add transform if missing
|
||||
TransformComponent transform;
|
||||
transform.node = sceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
e.set<TransformComponent>(transform);
|
||||
}
|
||||
e.set<CameraComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<CameraComponent>()) {
|
||||
auto& camera = e.get_mut<CameraComponent>();
|
||||
// Clean up Ogre camera and preview resources
|
||||
if (camera.camera) {
|
||||
// Clean up preview
|
||||
if (camera.previewTarget) {
|
||||
camera.previewTarget->removeAllListeners();
|
||||
}
|
||||
if (camera.previewTexture) {
|
||||
Ogre::TextureManager::getSingleton().remove(
|
||||
camera.previewTexture->getName());
|
||||
}
|
||||
|
||||
// Detach and destroy camera
|
||||
Ogre::SceneNode* parent = camera.camera->getParentSceneNode();
|
||||
if (parent) {
|
||||
parent->detachObject(camera.camera);
|
||||
}
|
||||
sceneMgr->destroyCamera(camera.camera);
|
||||
camera.camera = nullptr;
|
||||
}
|
||||
e.remove<CameraComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
178
src/features/editScene/components/CellGrid.cpp
Normal file
178
src/features/editScene/components/CellGrid.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
#include "CellGrid.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
Cell* CellGridComponent::findCell(int x, int y, int z)
|
||||
{
|
||||
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
|
||||
for (auto& cell : cells) {
|
||||
if (cell.getKey() == key) {
|
||||
return &cell;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Cell* CellGridComponent::findCell(int x, int y, int z) const
|
||||
{
|
||||
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
|
||||
for (const auto& cell : cells) {
|
||||
if (cell.getKey() == key) {
|
||||
return &cell;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Cell& CellGridComponent::getOrCreateCell(int x, int y, int z)
|
||||
{
|
||||
Cell* existing = findCell(x, y, z);
|
||||
if (existing) {
|
||||
return *existing;
|
||||
}
|
||||
Cell newCell;
|
||||
newCell.x = x;
|
||||
newCell.y = y;
|
||||
newCell.z = z;
|
||||
cells.push_back(newCell);
|
||||
return cells.back();
|
||||
}
|
||||
|
||||
void CellGridComponent::removeCell(int x, int y, int z)
|
||||
{
|
||||
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
|
||||
auto it = std::remove_if(cells.begin(), cells.end(),
|
||||
[key](const Cell& c) { return c.getKey() == key; });
|
||||
cells.erase(it, cells.end());
|
||||
}
|
||||
|
||||
FurnitureCell* CellGridComponent::findFurnitureCell(int x, int y, int z)
|
||||
{
|
||||
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
|
||||
for (auto& cell : furnitureCells) {
|
||||
if (cell.getKey() == key) {
|
||||
return &cell;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FurnitureCell& CellGridComponent::getOrCreateFurnitureCell(int x, int y, int z)
|
||||
{
|
||||
FurnitureCell* existing = findFurnitureCell(x, y, z);
|
||||
if (existing) {
|
||||
return *existing;
|
||||
}
|
||||
FurnitureCell newCell;
|
||||
newCell.x = x;
|
||||
newCell.y = y;
|
||||
newCell.z = z;
|
||||
furnitureCells.push_back(newCell);
|
||||
return furnitureCells.back();
|
||||
}
|
||||
|
||||
void CellGridComponent::removeFurnitureCell(int x, int y, int z)
|
||||
{
|
||||
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
|
||||
auto it = std::remove_if(furnitureCells.begin(), furnitureCells.end(),
|
||||
[key](const FurnitureCell& c) { return c.getKey() == key; });
|
||||
furnitureCells.erase(it, furnitureCells.end());
|
||||
}
|
||||
|
||||
void CellGridComponent::clear()
|
||||
{
|
||||
cells.clear();
|
||||
furnitureCells.clear();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
Ogre::Vector3 CellGridComponent::cellToWorld(int x, int y, int z) const
|
||||
{
|
||||
return Ogre::Vector3(
|
||||
x * cellSize,
|
||||
y * cellHeight,
|
||||
z * cellSize
|
||||
);
|
||||
}
|
||||
|
||||
void CellGridComponent::worldToCell(const Ogre::Vector3& worldPos, int& x, int& y, int& z) const
|
||||
{
|
||||
x = Ogre::Math::Floor(worldPos.x / cellSize);
|
||||
y = Ogre::Math::Floor(worldPos.y / cellHeight);
|
||||
z = Ogre::Math::Floor(worldPos.z / cellSize);
|
||||
}
|
||||
|
||||
const char* CellGridComponent::getFlagName(uint64_t flag)
|
||||
{
|
||||
switch (flag) {
|
||||
case CellFlags::Floor: return "Floor";
|
||||
case CellFlags::Ceiling: return "Ceiling";
|
||||
case CellFlags::WallXNeg: return "Ext Wall X-";
|
||||
case CellFlags::WallXPos: return "Ext Wall X+";
|
||||
case CellFlags::WallZPos: return "Ext Wall Z+";
|
||||
case CellFlags::WallZNeg: return "Ext Wall Z-";
|
||||
case CellFlags::DoorXNeg: return "Ext Door X-";
|
||||
case CellFlags::DoorXPos: return "Ext Door X+";
|
||||
case CellFlags::DoorZPos: return "Ext Door Z+";
|
||||
case CellFlags::DoorZNeg: return "Ext Door Z-";
|
||||
case CellFlags::WindowXNeg: return "Ext Window X-";
|
||||
case CellFlags::WindowXPos: return "Ext Window X+";
|
||||
case CellFlags::WindowZPos: return "Ext Window Z+";
|
||||
case CellFlags::WindowZNeg: return "Ext Window Z-";
|
||||
case CellFlags::IntWallXNeg: return "Int Wall X-";
|
||||
case CellFlags::IntWallXPos: return "Int Wall X+";
|
||||
case CellFlags::IntWallZPos: return "Int Wall Z+";
|
||||
case CellFlags::IntWallZNeg: return "Int Wall Z-";
|
||||
case CellFlags::IntDoorXNeg: return "Int Door X-";
|
||||
case CellFlags::IntDoorXPos: return "Int Door X+";
|
||||
case CellFlags::IntDoorZPos: return "Int Door Z+";
|
||||
case CellFlags::IntDoorZNeg: return "Int Door Z-";
|
||||
case CellFlags::IntWindowXNeg: return "Int Window X-";
|
||||
case CellFlags::IntWindowXPos: return "Int Window X+";
|
||||
case CellFlags::IntWindowZPos: return "Int Window Z+";
|
||||
case CellFlags::IntWindowZNeg: return "Int Window Z-";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t CellGridComponent::getFlagByName(const std::string& name)
|
||||
{
|
||||
auto flags = getAllFlags();
|
||||
for (const auto& [flag, flagName] : flags) {
|
||||
if (name == flagName) {
|
||||
return flag;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<std::pair<uint64_t, const char*>> CellGridComponent::getAllFlags()
|
||||
{
|
||||
return {
|
||||
{ CellFlags::Floor, "Floor" },
|
||||
{ CellFlags::Ceiling, "Ceiling" },
|
||||
{ CellFlags::WallXNeg, "Ext Wall X-" },
|
||||
{ CellFlags::WallXPos, "Ext Wall X+" },
|
||||
{ CellFlags::WallZPos, "Ext Wall Z+" },
|
||||
{ CellFlags::WallZNeg, "Ext Wall Z-" },
|
||||
{ CellFlags::DoorXNeg, "Ext Door X-" },
|
||||
{ CellFlags::DoorXPos, "Ext Door X+" },
|
||||
{ CellFlags::DoorZPos, "Ext Door Z+" },
|
||||
{ CellFlags::DoorZNeg, "Ext Door Z-" },
|
||||
{ CellFlags::WindowXNeg, "Ext Window X-" },
|
||||
{ CellFlags::WindowXPos, "Ext Window X+" },
|
||||
{ CellFlags::WindowZPos, "Ext Window Z+" },
|
||||
{ CellFlags::WindowZNeg, "Ext Window Z-" },
|
||||
{ CellFlags::IntWallXNeg, "Int Wall X-" },
|
||||
{ CellFlags::IntWallXPos, "Int Wall X+" },
|
||||
{ CellFlags::IntWallZPos, "Int Wall Z+" },
|
||||
{ CellFlags::IntWallZNeg, "Int Wall Z-" },
|
||||
{ CellFlags::IntDoorXNeg, "Int Door X-" },
|
||||
{ CellFlags::IntDoorXPos, "Int Door X+" },
|
||||
{ CellFlags::IntDoorZPos, "Int Door Z+" },
|
||||
{ CellFlags::IntDoorZNeg, "Int Door Z-" },
|
||||
{ CellFlags::IntWindowXNeg, "Int Window X-" },
|
||||
{ CellFlags::IntWindowXPos, "Int Window X+" },
|
||||
{ CellFlags::IntWindowZPos, "Int Window Z+" },
|
||||
{ CellFlags::IntWindowZNeg, "Int Window Z-" },
|
||||
};
|
||||
}
|
||||
521
src/features/editScene/components/CellGrid.hpp
Normal file
521
src/features/editScene/components/CellGrid.hpp
Normal file
@@ -0,0 +1,521 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <chrono>
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* @brief Cell flags for building geometry
|
||||
*
|
||||
* Each cell in the 3D grid can have these features:
|
||||
* - Floor/Ceiling
|
||||
* - External walls (4 directions)
|
||||
* - External doors/windows (4 directions each)
|
||||
* - Internal walls (4 directions)
|
||||
* - Internal doors/windows (4 directions each)
|
||||
*/
|
||||
namespace CellFlags {
|
||||
constexpr uint64_t Floor = 1ULL << 0;
|
||||
constexpr uint64_t Ceiling = 1ULL << 1;
|
||||
// External walls
|
||||
constexpr uint64_t WallXNeg = 1ULL << 2; // Wall on -X side
|
||||
constexpr uint64_t WallXPos = 1ULL << 3; // Wall on +X side
|
||||
constexpr uint64_t WallZPos = 1ULL << 4; // Wall on +Z side
|
||||
constexpr uint64_t WallZNeg = 1ULL << 5; // Wall on -Z side
|
||||
// External doors
|
||||
constexpr uint64_t DoorXNeg = 1ULL << 6;
|
||||
constexpr uint64_t DoorXPos = 1ULL << 7;
|
||||
constexpr uint64_t DoorZPos = 1ULL << 8;
|
||||
constexpr uint64_t DoorZNeg = 1ULL << 9;
|
||||
// External windows
|
||||
constexpr uint64_t WindowXNeg = 1ULL << 10;
|
||||
constexpr uint64_t WindowXPos = 1ULL << 11;
|
||||
constexpr uint64_t WindowZPos = 1ULL << 12;
|
||||
constexpr uint64_t WindowZNeg = 1ULL << 13;
|
||||
// Internal walls
|
||||
constexpr uint64_t IntWallXNeg = 1ULL << 14;
|
||||
constexpr uint64_t IntWallXPos = 1ULL << 15;
|
||||
constexpr uint64_t IntWallZPos = 1ULL << 16;
|
||||
constexpr uint64_t IntWallZNeg = 1ULL << 17;
|
||||
// Internal doors
|
||||
constexpr uint64_t IntDoorXNeg = 1ULL << 18;
|
||||
constexpr uint64_t IntDoorXPos = 1ULL << 19;
|
||||
constexpr uint64_t IntDoorZPos = 1ULL << 20;
|
||||
constexpr uint64_t IntDoorZNeg = 1ULL << 21;
|
||||
// Internal windows
|
||||
constexpr uint64_t IntWindowXNeg = 1ULL << 22;
|
||||
constexpr uint64_t IntWindowXPos = 1ULL << 23;
|
||||
constexpr uint64_t IntWindowZPos = 1ULL << 24;
|
||||
constexpr uint64_t IntWindowZNeg = 1ULL << 25;
|
||||
|
||||
// Masks
|
||||
constexpr uint64_t AllExternalWalls = WallXNeg | WallXPos | WallZPos | WallZNeg;
|
||||
constexpr uint64_t AllInternalWalls = IntWallXNeg | IntWallXPos | IntWallZPos | IntWallZNeg;
|
||||
constexpr uint64_t AllWalls = AllExternalWalls | AllInternalWalls;
|
||||
constexpr uint64_t AllDoors = DoorXNeg | DoorXPos | DoorZPos | DoorZNeg |
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A single cell in the 3D grid
|
||||
*/
|
||||
struct Cell {
|
||||
int x = 0, y = 0, z = 0;
|
||||
uint64_t flags = 0;
|
||||
|
||||
// Generate unique key for this cell
|
||||
int64_t getKey() const {
|
||||
// Support -1024 to 1024 range for each axis
|
||||
return (int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z;
|
||||
}
|
||||
|
||||
bool hasFlag(uint64_t flag) const { return (flags & flag) != 0; }
|
||||
void setFlag(uint64_t flag) { flags |= flag; }
|
||||
void clearFlag(uint64_t flag) { flags &= ~flag; }
|
||||
void toggleFlag(uint64_t flag) { flags ^= flag; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Furniture placement in a cell
|
||||
*/
|
||||
struct FurnitureCell {
|
||||
int x = 0, y = 0, z = 0;
|
||||
std::vector<std::string> tags;
|
||||
std::string furnitureType;
|
||||
int rotation = 0; // 0-3, representing 0, 90, 180, 270 degrees
|
||||
|
||||
int64_t getKey() const {
|
||||
return (int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Cell grid for procedural building generation
|
||||
*
|
||||
* This component stores a sparse 3D grid of cells that define
|
||||
* building geometry (walls, floors, doors, windows).
|
||||
*
|
||||
* Used by: House/Lot generation, dungeon generation
|
||||
*/
|
||||
struct CellGridComponent {
|
||||
// Grid dimensions (in cells)
|
||||
int width = 10; // X dimension
|
||||
int height = 1; // Y dimension (floors)
|
||||
int depth = 10; // Z dimension
|
||||
|
||||
// Cell size in world units
|
||||
float cellSize = 4.0f;
|
||||
float cellHeight = 4.0f;
|
||||
|
||||
// The cell data (sparse storage)
|
||||
std::vector<Cell> cells;
|
||||
std::vector<FurnitureCell> furnitureCells;
|
||||
|
||||
// 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;
|
||||
|
||||
// Find cell at position (returns nullptr if not found)
|
||||
Cell* findCell(int x, int y, int z);
|
||||
const Cell* findCell(int x, int y, int z) const;
|
||||
|
||||
// Get or create cell at position
|
||||
Cell& getOrCreateCell(int x, int y, int z);
|
||||
|
||||
// Remove cell at position
|
||||
void removeCell(int x, int y, int z);
|
||||
|
||||
// Find furniture cell
|
||||
FurnitureCell* findFurnitureCell(int x, int y, int z);
|
||||
|
||||
// Get or create furniture cell
|
||||
FurnitureCell& getOrCreateFurnitureCell(int x, int y, int z);
|
||||
|
||||
// Remove furniture cell
|
||||
void removeFurnitureCell(int x, int y, int z);
|
||||
|
||||
// Clear all cells
|
||||
void clear();
|
||||
|
||||
// Mark for rebuild
|
||||
void markDirty() { dirty = true; version++; }
|
||||
|
||||
// Convert local cell position to world position
|
||||
Ogre::Vector3 cellToWorld(int x, int y, int z) const;
|
||||
|
||||
// Convert world position to cell coordinates
|
||||
void worldToCell(const Ogre::Vector3& worldPos, int& x, int& y, int& z) const;
|
||||
|
||||
// Get bit name for editor display
|
||||
static const char* getFlagName(uint64_t flag);
|
||||
static uint64_t getFlagByName(const std::string& name);
|
||||
static std::vector<std::pair<uint64_t, const char*>> getAllFlags();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Room definition within a cell grid
|
||||
*
|
||||
* 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)
|
||||
// A room from (minX, minZ) to (maxX-1, maxZ-1) inclusive
|
||||
int minX = 0, minY = 0, minZ = 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;
|
||||
|
||||
// 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
|
||||
|
||||
// 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; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Roof definition for a lot
|
||||
*/
|
||||
struct RoofComponent {
|
||||
enum Type {
|
||||
Flat = 0,
|
||||
Normal = 1,
|
||||
Normal2 = 2,
|
||||
Cone = 3,
|
||||
Cylinder = 4
|
||||
};
|
||||
|
||||
Type type = Flat;
|
||||
|
||||
// Position (in cell coordinates)
|
||||
int posX = 0, posY = 0, posZ = 0;
|
||||
|
||||
// Size (in cells)
|
||||
int sizeX = 1, sizeZ = 1;
|
||||
|
||||
// Offset from cell position (world units)
|
||||
float offsetX = 0.0f, offsetY = 0.0f, offsetZ = 0.0f;
|
||||
|
||||
// Height parameters
|
||||
float baseHeight = 0.5f;
|
||||
float maxHeight = 0.5f;
|
||||
|
||||
// Dirty flag - triggers rebuild when changed
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Lot component - represents a single house/building lot
|
||||
*
|
||||
* A lot contains:
|
||||
* - Cell grid (walls, floors, etc.)
|
||||
* - Room definitions
|
||||
* - Roof definitions
|
||||
* - Furniture placement rules
|
||||
*/
|
||||
struct LotComponent {
|
||||
// Lot dimensions (in cells)
|
||||
int width = 10;
|
||||
int depth = 10;
|
||||
|
||||
// Elevation offset from district
|
||||
float elevation = 0.0f;
|
||||
|
||||
// Rotation around district center (degrees)
|
||||
float angle = 0.0f;
|
||||
|
||||
// Position offset (for manual placement)
|
||||
float offsetX = 0.0f, offsetZ = 0.0f;
|
||||
|
||||
// Template name if this lot was created from a template
|
||||
std::string templateName;
|
||||
|
||||
// Procedural material for lot base (optional - falls back to District, then Town)
|
||||
flecs::entity proceduralMaterialEntity;
|
||||
std::string proceduralMaterialEntityId; // For serialization
|
||||
|
||||
// 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;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief District component - contains multiple lots arranged in a circle
|
||||
*/
|
||||
struct DistrictComponent {
|
||||
// District radius (distance from center to lots)
|
||||
float radius = 50.0f;
|
||||
|
||||
// Elevation offset
|
||||
float elevation = 0.0f;
|
||||
|
||||
// Height of district base (for plaza)
|
||||
float height = 0.2f;
|
||||
|
||||
// Is this a plaza (circular base)?
|
||||
bool isPlaza = false;
|
||||
|
||||
// Lot templates for this district
|
||||
std::vector<std::string> lotTemplateNames;
|
||||
|
||||
// Procedural material for plaza (references entity with ProceduralMaterialComponent)
|
||||
flecs::entity proceduralMaterialEntity;
|
||||
std::string proceduralMaterialEntityId; // For serialization
|
||||
|
||||
// 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;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Town component - top-level container
|
||||
*/
|
||||
struct TownComponent {
|
||||
std::string townName = "New Town";
|
||||
|
||||
// Color rectangles for texture atlas
|
||||
struct ColorRect {
|
||||
float left = 0.0f, top = 0.0f, right = 1.0f, bottom = 1.0f;
|
||||
Ogre::ColourValue color = Ogre::ColourValue::White;
|
||||
};
|
||||
std::unordered_map<std::string, ColorRect> colorRects;
|
||||
|
||||
// Material name (created from color rects)
|
||||
std::string materialName;
|
||||
|
||||
// Procedural material for town (used by districts and lots)
|
||||
flecs::entity proceduralMaterialEntity;
|
||||
std::string proceduralMaterialEntityId; // For serialization
|
||||
|
||||
// Texture rectangle name from ProceduralTexture for UV mapping
|
||||
std::string textureRectName;
|
||||
|
||||
// Dirty flag
|
||||
bool dirty = true;
|
||||
bool materialDirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
void markMaterialDirty() { materialDirty = true; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Furniture template - defines a furniture item
|
||||
*/
|
||||
struct FurnitureTemplateComponent {
|
||||
std::string templateName;
|
||||
std::vector<std::string> tags; // e.g., "bed", "essential", "bedroom"
|
||||
|
||||
// Mesh/model info
|
||||
std::string meshName;
|
||||
std::string materialName;
|
||||
|
||||
// Placement rules
|
||||
bool requiresWall = false;
|
||||
bool requiresFloor = true;
|
||||
bool blocksPath = true;
|
||||
|
||||
// Offset from cell center
|
||||
float offsetX = 0.0f, offsetY = 0.0f, offsetZ = 0.0f;
|
||||
|
||||
// Random selection weight
|
||||
float weight = 1.0f;
|
||||
};
|
||||
167
src/features/editScene/components/CellGridEditorsModule.cpp
Normal file
167
src/features/editScene/components/CellGridEditorsModule.cpp
Normal file
@@ -0,0 +1,167 @@
|
||||
#include "CellGrid.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/CellGridEditor.hpp"
|
||||
#include "../ui/LotEditor.hpp"
|
||||
#include "../ui/DistrictEditor.hpp"
|
||||
#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_GROUP("Cell Grid", "Cell Grid", CellGridComponent, CellGridEditor)
|
||||
{
|
||||
registry.registerComponent<CellGridComponent>(
|
||||
"Cell Grid", "Cell Grid",
|
||||
std::make_unique<CellGridEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<CellGridComponent>()) {
|
||||
e.set<CellGridComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<CellGridComponent>()) {
|
||||
e.remove<CellGridComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register Lot component
|
||||
REGISTER_COMPONENT_GROUP("Lot", "Cell Grid", LotComponent, LotEditor)
|
||||
{
|
||||
registry.registerComponent<LotComponent>(
|
||||
"Lot", "Cell Grid",
|
||||
std::make_unique<LotEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<LotComponent>()) {
|
||||
e.set<LotComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<LotComponent>()) {
|
||||
e.remove<LotComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register District component
|
||||
REGISTER_COMPONENT_GROUP("District", "Cell Grid", DistrictComponent, DistrictEditor)
|
||||
{
|
||||
registry.registerComponent<DistrictComponent>(
|
||||
"District", "Cell Grid",
|
||||
std::make_unique<DistrictEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<DistrictComponent>()) {
|
||||
e.set<DistrictComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<DistrictComponent>()) {
|
||||
e.remove<DistrictComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register Town component
|
||||
REGISTER_COMPONENT_GROUP("Town", "Cell Grid", TownComponent, TownEditor)
|
||||
{
|
||||
registry.registerComponent<TownComponent>(
|
||||
"Town", "Cell Grid",
|
||||
std::make_unique<TownEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<TownComponent>()) {
|
||||
e.set<TownComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<TownComponent>()) {
|
||||
e.remove<TownComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register Roof component
|
||||
REGISTER_COMPONENT_GROUP("Roof", "Cell Grid", RoofComponent, RoofEditor)
|
||||
{
|
||||
registry.registerComponent<RoofComponent>(
|
||||
"Roof", "Cell Grid",
|
||||
std::make_unique<RoofEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<RoofComponent>()) {
|
||||
e.set<RoofComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<RoofComponent>()) {
|
||||
e.remove<RoofComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register Room component (in Room Layout group)
|
||||
REGISTER_COMPONENT_GROUP("Room", "Room Layout", RoomComponent, RoomEditor)
|
||||
{
|
||||
registry.registerComponent<RoomComponent>(
|
||||
"Room", "Room Layout",
|
||||
std::make_unique<RoomEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<RoomComponent>()) {
|
||||
e.set<RoomComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<RoomComponent>()) {
|
||||
e.remove<RoomComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register FurnitureTemplate component
|
||||
REGISTER_COMPONENT_GROUP("Furniture Template", "Cell Grid", FurnitureTemplateComponent, FurnitureTemplateEditor)
|
||||
{
|
||||
registry.registerComponent<FurnitureTemplateComponent>(
|
||||
"Furniture Template", "Cell Grid",
|
||||
std::make_unique<FurnitureTemplateEditor>(),
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<FurnitureTemplateComponent>()) {
|
||||
e.set<FurnitureTemplateComponent>({});
|
||||
}
|
||||
},
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<FurnitureTemplateComponent>()) {
|
||||
e.remove<FurnitureTemplateComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 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.
|
||||
48
src/features/editScene/components/CellGridModule.cpp
Normal file
48
src/features/editScene/components/CellGridModule.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include "CellGridModule.hpp"
|
||||
#include "CellGrid.hpp"
|
||||
#include "GeneratedPhysicsTag.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <Ogre.h>
|
||||
|
||||
namespace CellGridModule {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
void registerComponents(flecs::world& world)
|
||||
{
|
||||
// Register components without .member<> for complex types (std::string, std::vector)
|
||||
// Flecs doesn't have built-in reflection for these, but JSON serialization handles them
|
||||
|
||||
// CellGridComponent
|
||||
world.component<CellGridComponent>();
|
||||
|
||||
// Cell struct
|
||||
world.component<Cell>();
|
||||
|
||||
// FurnitureCell struct
|
||||
world.component<FurnitureCell>();
|
||||
|
||||
// LotComponent
|
||||
world.component<LotComponent>();
|
||||
|
||||
// DistrictComponent
|
||||
world.component<DistrictComponent>();
|
||||
|
||||
// TownComponent
|
||||
world.component<TownComponent>();
|
||||
|
||||
// TownComponent::ColorRect
|
||||
world.component<TownComponent::ColorRect>();
|
||||
|
||||
// RoomComponent
|
||||
world.component<RoomComponent>();
|
||||
|
||||
// RoofComponent
|
||||
world.component<RoofComponent>();
|
||||
|
||||
// FurnitureTemplateComponent
|
||||
world.component<FurnitureTemplateComponent>();
|
||||
world.component<GeneratedPhysicsTag>();
|
||||
}
|
||||
|
||||
} // namespace CellGridModule
|
||||
7
src/features/editScene/components/CellGridModule.hpp
Normal file
7
src/features/editScene/components/CellGridModule.hpp
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
|
||||
namespace CellGridModule {
|
||||
void registerComponents(flecs::world& world);
|
||||
}
|
||||
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>();
|
||||
});
|
||||
}
|
||||
48
src/features/editScene/components/Light.hpp
Normal file
48
src/features/editScene/components/Light.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#ifndef EDITSCENE_LIGHT_HPP
|
||||
#define EDITSCENE_LIGHT_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Light component - attaches an Ogre::Light to the entity's SceneNode
|
||||
*/
|
||||
struct LightComponent {
|
||||
enum class LightType {
|
||||
Point, // Omnidirectional light
|
||||
Directional, // Parallel rays (sun/moon)
|
||||
Spotlight // Cone-shaped light
|
||||
};
|
||||
|
||||
LightType lightType = LightType::Point;
|
||||
|
||||
// Common properties
|
||||
Ogre::ColourValue diffuseColor{0.8f, 0.8f, 0.8f};
|
||||
Ogre::ColourValue specularColor{0.5f, 0.5f, 0.5f};
|
||||
float intensity = 1.0f;
|
||||
|
||||
// Attenuation (for Point and Spot)
|
||||
float range = 100.0f;
|
||||
float constantAttenuation = 1.0f;
|
||||
float linearAttenuation = 0.0f;
|
||||
float quadraticAttenuation = 0.0f;
|
||||
|
||||
// Spotlight specific
|
||||
float spotlightInnerAngle = 30.0f; // Degrees
|
||||
float spotlightOuterAngle = 45.0f; // Degrees
|
||||
float spotlightFalloff = 1.0f;
|
||||
|
||||
// Direction (for Directional and Spot, relative to node orientation)
|
||||
Ogre::Vector3 direction{0, -1, 0};
|
||||
|
||||
// Cast shadows
|
||||
bool castShadows = true;
|
||||
|
||||
// The Ogre light object (created by LightSystem)
|
||||
Ogre::Light* light = nullptr;
|
||||
|
||||
void markDirty() { needsRebuild = true; }
|
||||
bool needsRebuild = true;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_LIGHT_HPP
|
||||
43
src/features/editScene/components/LightModule.cpp
Normal file
43
src/features/editScene/components/LightModule.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "Light.hpp"
|
||||
#include "Transform.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/LightEditor.hpp"
|
||||
#include "../systems/LightSystem.hpp"
|
||||
|
||||
// Register Light component
|
||||
REGISTER_COMPONENT("Light", LightComponent, LightEditor)
|
||||
{
|
||||
registry.registerComponent<LightComponent>(
|
||||
"Light",
|
||||
std::make_unique<LightEditor>(),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<LightComponent>()) {
|
||||
// Light requires Transform
|
||||
if (!e.has<TransformComponent>()) {
|
||||
// Auto-add transform if missing
|
||||
TransformComponent transform;
|
||||
transform.node = sceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
e.set<TransformComponent>(transform);
|
||||
}
|
||||
e.set<LightComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<LightComponent>()) {
|
||||
auto& light = e.get_mut<LightComponent>();
|
||||
// Clean up Ogre light
|
||||
if (light.light) {
|
||||
Ogre::SceneNode* parent = light.light->getParentSceneNode();
|
||||
if (parent) {
|
||||
parent->detachObject(light.light);
|
||||
}
|
||||
sceneMgr->destroyLight(light.light);
|
||||
light.light = nullptr;
|
||||
}
|
||||
e.remove<LightComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
47
src/features/editScene/components/Lod.hpp
Normal file
47
src/features/editScene/components/Lod.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#ifndef EDITSCENE_LOD_HPP
|
||||
#define EDITSCENE_LOD_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
#include <string>
|
||||
#include "LodSettings.hpp"
|
||||
|
||||
/**
|
||||
* LodComponent - Per-entity LOD configuration
|
||||
*
|
||||
* This component references a LodSettingsComponent by its settingsId
|
||||
* for persistent identification across scene loads.
|
||||
*/
|
||||
struct LodComponent {
|
||||
// Reference to the settings by ID (persistent across scene loads)
|
||||
std::string settingsId;
|
||||
|
||||
// Runtime entity reference (resolved from settingsId)
|
||||
flecs::entity settingsEntity = flecs::entity::null();
|
||||
|
||||
// Per-entity distance multiplier (scales all LOD distances)
|
||||
float distanceMultiplier = 1.0f;
|
||||
|
||||
// Whether LOD has been applied to the mesh
|
||||
bool lodApplied = false;
|
||||
|
||||
// Last settings version that was applied
|
||||
uint32_t appliedVersion = 0;
|
||||
|
||||
// Dirty flag for regenerating LOD
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
|
||||
// Helper to check if LOD needs to be regenerated
|
||||
bool needsUpdate() const { return dirty; }
|
||||
|
||||
// Helper to check if settings reference is valid
|
||||
bool hasValidSettings() const {
|
||||
return settingsEntity.is_alive() &&
|
||||
settingsEntity.has<LodSettingsComponent>();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_LOD_HPP
|
||||
56
src/features/editScene/components/LodModule.cpp
Normal file
56
src/features/editScene/components/LodModule.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#include "Lod.hpp"
|
||||
#include "LodSettings.hpp"
|
||||
#include "Renderable.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/LodEditor.hpp"
|
||||
#include "../ui/LodSettingsEditor.hpp"
|
||||
|
||||
// Register LodSettings component
|
||||
REGISTER_COMPONENT("LOD Settings", LodSettingsComponent, LodSettingsEditor)
|
||||
{
|
||||
registry.registerComponent<LodSettingsComponent>(
|
||||
"LOD Settings",
|
||||
std::make_unique<LodSettingsEditor>(),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<LodSettingsComponent>()) {
|
||||
auto settings = LodSettingsComponent();
|
||||
// Create default LOD levels
|
||||
settings.createDefaultLevels();
|
||||
e.set<LodSettingsComponent>(settings);
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<LodSettingsComponent>()) {
|
||||
e.remove<LodSettingsComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register Lod component
|
||||
REGISTER_COMPONENT("LOD", LodComponent, LodEditor)
|
||||
{
|
||||
registry.registerComponent<LodComponent>(
|
||||
"LOD",
|
||||
std::make_unique<LodEditor>(),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<LodComponent>()) {
|
||||
// LOD requires Renderable
|
||||
if (!e.has<RenderableComponent>()) {
|
||||
// Auto-add renderable if missing (will need mesh assignment later)
|
||||
e.set<RenderableComponent>({});
|
||||
}
|
||||
e.set<LodComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<LodComponent>()) {
|
||||
e.remove<LodComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
107
src/features/editScene/components/LodSettings.hpp
Normal file
107
src/features/editScene/components/LodSettings.hpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#ifndef EDITSCENE_LODSETTINGS_HPP
|
||||
#define EDITSCENE_LODSETTINGS_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* LOD Level configuration - represents one LOD level
|
||||
*/
|
||||
struct LodLevelConfig {
|
||||
// Distance at which to switch to this LOD level
|
||||
float distance = 100.0f;
|
||||
|
||||
// Reduction method
|
||||
enum class ReductionMethod {
|
||||
Proportional, // 0.0-1.0 percentage of vertices to remove
|
||||
Constant, // Exact vertex count to remove
|
||||
CollapseCost // Collapse cost threshold
|
||||
};
|
||||
ReductionMethod reductionMethod = ReductionMethod::Proportional;
|
||||
|
||||
// Reduction value (meaning depends on method)
|
||||
float reductionValue = 0.5f; // 50% reduction by default
|
||||
|
||||
// Optional: manual mesh name (if using manual LOD)
|
||||
std::string manualMeshName;
|
||||
|
||||
bool useManualMesh = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* LodSettingsComponent - Shareable LOD configuration
|
||||
*
|
||||
* This component can be attached to a "settings" entity and referenced
|
||||
* by multiple entities with LodComponent for shared LOD settings.
|
||||
*/
|
||||
struct LodSettingsComponent {
|
||||
// Unique identifier for this LOD settings (for referencing across scenes)
|
||||
std::string settingsId;
|
||||
|
||||
// Strategy for determining LOD level based on distance
|
||||
enum class Strategy {
|
||||
Distance, // Distance from camera
|
||||
PixelCount, // Screen-space pixel count
|
||||
EdgePixelCount // Screen-space edge pixel count
|
||||
};
|
||||
Strategy strategy = Strategy::Distance;
|
||||
|
||||
// LOD levels (from highest quality to lowest)
|
||||
// Level 0 is always the original mesh
|
||||
std::vector<LodLevelConfig> lodLevels;
|
||||
|
||||
// Advanced options
|
||||
bool useCompression = true; // Compress index buffers
|
||||
bool useVertexNormals = true; // Use normals for quality
|
||||
bool preventPunchingHoles = false; // Prevent destroying triangles
|
||||
bool preventBreakingLines = false; // Prevent destroying lines
|
||||
bool useBackgroundQueue = false; // Generate LODs in background
|
||||
|
||||
float outsideWeight = 0.0f; // Weight for outside faces (0 = disabled)
|
||||
float outsideWalkAngle = 0.0f; // Angle for outside walking (-1 to 1)
|
||||
|
||||
// Flag to track if settings have been modified
|
||||
bool dirty = true;
|
||||
|
||||
// Version counter for tracking changes
|
||||
uint32_t version = 1;
|
||||
|
||||
void markDirty() {
|
||||
dirty = true;
|
||||
version++;
|
||||
}
|
||||
|
||||
// Helper to add a proportional reduction level
|
||||
void addProportionalLevel(float distance, float reductionPercent) {
|
||||
LodLevelConfig level;
|
||||
level.distance = distance;
|
||||
level.reductionMethod = LodLevelConfig::ReductionMethod::Proportional;
|
||||
level.reductionValue = reductionPercent;
|
||||
lodLevels.push_back(level);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
// Helper to add a constant reduction level
|
||||
void addConstantLevel(float distance, int verticesToRemove) {
|
||||
LodLevelConfig level;
|
||||
level.distance = distance;
|
||||
level.reductionMethod = LodLevelConfig::ReductionMethod::Constant;
|
||||
level.reductionValue = static_cast<float>(verticesToRemove);
|
||||
lodLevels.push_back(level);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
// Helper to create default LOD levels for a mesh
|
||||
void createDefaultLevels(float baseDistance = 100.0f) {
|
||||
lodLevels.clear();
|
||||
// LOD 1: 50% reduction at base distance
|
||||
addProportionalLevel(baseDistance, 0.5f);
|
||||
// LOD 2: 75% reduction at 2x distance
|
||||
addProportionalLevel(baseDistance * 2.0f, 0.75f);
|
||||
markDirty();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_LODSETTINGS_HPP
|
||||
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
|
||||
37
src/features/editScene/components/Primitive.hpp
Normal file
37
src/features/editScene/components/Primitive.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <flecs.h>
|
||||
|
||||
/**
|
||||
* @brief Base component for procedural primitives that feed into TriangleBuffer
|
||||
*/
|
||||
struct PrimitiveComponent {
|
||||
// Type of primitive
|
||||
enum class Type {
|
||||
None,
|
||||
Box,
|
||||
Plane
|
||||
};
|
||||
|
||||
Type type = Type::None;
|
||||
|
||||
// Box parameters
|
||||
float boxSizeX = 1.0f;
|
||||
float boxSizeY = 1.0f;
|
||||
float boxSizeZ = 1.0f;
|
||||
|
||||
// Plane parameters
|
||||
float planeSizeX = 1.0f;
|
||||
float planeSizeY = 1.0f;
|
||||
int planeSegmentsX = 1;
|
||||
int planeSegmentsY = 1;
|
||||
|
||||
// Dirty flag - triggers TriangleBuffer rebuild
|
||||
bool dirty = true;
|
||||
|
||||
// Track last seen transform version to detect gizmo movements
|
||||
unsigned int lastTransformVersion = 0;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
};
|
||||
24
src/features/editScene/components/PrimitiveModule.cpp
Normal file
24
src/features/editScene/components/PrimitiveModule.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include "Primitive.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/PrimitiveEditor.hpp"
|
||||
|
||||
// Register Primitive component
|
||||
REGISTER_COMPONENT("Primitive", PrimitiveComponent, PrimitiveEditor)
|
||||
{
|
||||
registry.registerComponent<PrimitiveComponent>(
|
||||
"Primitive",
|
||||
std::make_unique<PrimitiveEditor>(),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<PrimitiveComponent>()) {
|
||||
e.set<PrimitiveComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<PrimitiveComponent>()) {
|
||||
e.remove<PrimitiveComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
58
src/features/editScene/components/ProceduralMaterial.hpp
Normal file
58
src/features/editScene/components/ProceduralMaterial.hpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
#include "ProceduralTexture.hpp"
|
||||
|
||||
/**
|
||||
* @brief Component for creating procedural Ogre materials
|
||||
*
|
||||
* Creates a named material that uses ProceduralTexture as diffuse/albedo map.
|
||||
* The material can be referenced by name for use with meshes/entities.
|
||||
*/
|
||||
struct ProceduralMaterialComponent {
|
||||
// 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>();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "ProceduralMaterial.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ProceduralMaterialEditor.hpp"
|
||||
#include <OgreMaterialManager.h>
|
||||
#include <OgreRTShaderSystem.h>
|
||||
|
||||
// Register ProceduralMaterial component
|
||||
REGISTER_COMPONENT_GROUP("Procedural Material", "Material", ProceduralMaterialComponent, ProceduralMaterialEditor)
|
||||
{
|
||||
registry.registerComponent<ProceduralMaterialComponent>(
|
||||
"Procedural Material", "Material",
|
||||
std::make_unique<ProceduralMaterialEditor>(sceneMgr),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<ProceduralMaterialComponent>()) {
|
||||
e.set<ProceduralMaterialComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<ProceduralMaterialComponent>()) {
|
||||
auto& material = e.get_mut<ProceduralMaterialComponent>();
|
||||
// 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);
|
||||
}
|
||||
} catch (...) {}
|
||||
material.ogreMaterial.reset();
|
||||
}
|
||||
e.remove<ProceduralMaterialComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
183
src/features/editScene/components/ProceduralTexture.hpp
Normal file
183
src/features/editScene/components/ProceduralTexture.hpp
Normal file
@@ -0,0 +1,183 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* @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)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Component for procedural texture generation with 10x10 colored rectangles
|
||||
*
|
||||
* Uses OgreProcedural to generate a texture with a grid of colored rectangles.
|
||||
* Each rectangle color is individually selectable.
|
||||
* Also supports named rectangles for texture atlas/UV mapping.
|
||||
*/
|
||||
struct ProceduralTextureComponent {
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "ProceduralTexture.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ProceduralTextureEditor.hpp"
|
||||
|
||||
// Register ProceduralTexture component
|
||||
REGISTER_COMPONENT_GROUP("Procedural Texture", "Material", ProceduralTextureComponent, ProceduralTextureEditor)
|
||||
{
|
||||
registry.registerComponent<ProceduralTextureComponent>(
|
||||
"Procedural Texture", "Material",
|
||||
std::make_unique<ProceduralTextureEditor>(),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<ProceduralTextureComponent>()) {
|
||||
e.set<ProceduralTextureComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<ProceduralTextureComponent>()) {
|
||||
auto& texture = e.get_mut<ProceduralTextureComponent>();
|
||||
// Clean up Ogre texture - wrap in try/catch since TextureManager may be shutting down
|
||||
if (texture.ogreTexture) {
|
||||
try {
|
||||
if (Ogre::TextureManager::getSingletonPtr()) {
|
||||
Ogre::TextureManager::getSingleton().remove(texture.ogreTexture);
|
||||
}
|
||||
} catch (...) {}
|
||||
texture.ogreTexture.reset();
|
||||
}
|
||||
e.remove<ProceduralTextureComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,9 @@ struct RigidBodyComponent {
|
||||
float restitution = 0.0f; // Bounciness
|
||||
bool isSensor = false; // Detects collisions but doesn't respond
|
||||
|
||||
// Enable/disable physics body
|
||||
bool enabled = true; // If false, body is removed from physics world
|
||||
|
||||
// The Jolt body ID
|
||||
JPH::BodyID bodyID;
|
||||
bool bodyCreated = false;
|
||||
|
||||
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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
43
src/features/editScene/components/StaticGeometry.hpp
Normal file
43
src/features/editScene/components/StaticGeometry.hpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* @brief Component for a StaticGeometry region/batch
|
||||
*
|
||||
* Entities with StaticGeometryMemberComponent reference this region.
|
||||
* The system collects all members and builds the batched geometry.
|
||||
*/
|
||||
struct StaticGeometryComponent {
|
||||
// Persistent ID for serialization and member references
|
||||
std::string regionId;
|
||||
|
||||
// Region name for the Ogre::StaticGeometry object
|
||||
std::string regionName;
|
||||
|
||||
// The actual Ogre StaticGeometry object
|
||||
Ogre::StaticGeometry* staticGeometry = nullptr;
|
||||
|
||||
// Build configuration
|
||||
float renderingDistance = 0.0f; // 0 = infinite
|
||||
bool castShadows = true;
|
||||
|
||||
// Optimization settings
|
||||
float regionDimensions = 1000.0f; // Size of each region cell
|
||||
std::uint32_t visibilityFlags = 0xFFFFFFFF;
|
||||
|
||||
// State
|
||||
bool built = false;
|
||||
bool dirty = true; // Needs rebuild
|
||||
unsigned int buildVersion = 0; // Incremented on each build
|
||||
|
||||
// List of member entity IDs (for tracking)
|
||||
std::vector<std::uint64_t> memberEntityIds;
|
||||
|
||||
void markDirty() {
|
||||
dirty = true;
|
||||
}
|
||||
};
|
||||
45
src/features/editScene/components/StaticGeometryMember.hpp
Normal file
45
src/features/editScene/components/StaticGeometryMember.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <flecs.h>
|
||||
|
||||
/**
|
||||
* @brief Component for entities that should be batched into StaticGeometry
|
||||
*
|
||||
* This component stores mesh data and references the StaticGeometry region entity.
|
||||
* The actual Ogre::Entity is NOT created on a SceneNode - instead, the data is
|
||||
* used to add geometry to the StaticGeometry batch.
|
||||
*
|
||||
* Position and Orientation are taken from TransformComponent's SceneNode (_getDerived*).
|
||||
* Scale is also taken from TransformComponent.
|
||||
*/
|
||||
struct StaticGeometryMemberComponent {
|
||||
// Reference to the StaticGeometry region entity (runtime only)
|
||||
flecs::entity regionEntity;
|
||||
|
||||
// Persistent ID for serialization (references StaticGeometryComponent::regionId)
|
||||
std::string regionId;
|
||||
|
||||
// Mesh data (stored here, not in RenderableComponent)
|
||||
std::string meshName;
|
||||
std::string materialName; // Empty = use mesh default
|
||||
|
||||
// LOD settings reference (must be same for all members in a region)
|
||||
std::string lodSettingsId;
|
||||
flecs::entity lodSettingsEntity;
|
||||
|
||||
// Configuration
|
||||
bool castShadows = true;
|
||||
bool visible = true;
|
||||
|
||||
// Dirty flag - triggers region rebuild when changed
|
||||
bool dirty = true;
|
||||
|
||||
// Version tracking for transform changes
|
||||
unsigned int transformVersion = 0;
|
||||
|
||||
void markDirty() {
|
||||
dirty = true;
|
||||
transformVersion++;
|
||||
}
|
||||
};
|
||||
60
src/features/editScene/components/StaticGeometryModule.cpp
Normal file
60
src/features/editScene/components/StaticGeometryModule.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#include "StaticGeometry.hpp"
|
||||
#include "StaticGeometryMember.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/StaticGeometryEditor.hpp"
|
||||
#include "../ui/StaticGeometryMemberEditor.hpp"
|
||||
#include <Ogre.h>
|
||||
#include <random>
|
||||
|
||||
// Helper to generate unique ID
|
||||
static std::string generateRegionId() {
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_int_distribution<> dis(1000, 9999);
|
||||
return "region_" + std::to_string(dis(gen));
|
||||
}
|
||||
|
||||
// Register StaticGeometry (region) component
|
||||
REGISTER_COMPONENT("StaticGeometry Region", StaticGeometryComponent, StaticGeometryEditor)
|
||||
{
|
||||
registry.registerComponent<StaticGeometryComponent>(
|
||||
"StaticGeometry Region",
|
||||
std::make_unique<StaticGeometryEditor>(),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<StaticGeometryComponent>()) {
|
||||
auto region = StaticGeometryComponent();
|
||||
region.regionId = generateRegionId();
|
||||
region.regionName = "Region_" + Ogre::StringConverter::toString(e.id());
|
||||
e.set<StaticGeometryComponent>(region);
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<StaticGeometryComponent>()) {
|
||||
e.remove<StaticGeometryComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Register StaticGeometryMember component
|
||||
REGISTER_COMPONENT("StaticGeometry Member", StaticGeometryMemberComponent, StaticGeometryMemberEditor)
|
||||
{
|
||||
registry.registerComponent<StaticGeometryMemberComponent>(
|
||||
"StaticGeometry Member",
|
||||
std::make_unique<StaticGeometryMemberEditor>(sceneMgr),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<StaticGeometryMemberComponent>()) {
|
||||
e.set<StaticGeometryMemberComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<StaticGeometryMemberComponent>()) {
|
||||
e.remove<StaticGeometryMemberComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,9 @@ struct TransformComponent {
|
||||
Ogre::Vector3 position = Ogre::Vector3::ZERO;
|
||||
Ogre::Quaternion rotation = Ogre::Quaternion::IDENTITY;
|
||||
Ogre::Vector3 scale = Ogre::Vector3::UNIT_SCALE;
|
||||
|
||||
// Version tracking for change detection
|
||||
unsigned int version = 0;
|
||||
|
||||
/**
|
||||
* Apply component values to the scene node
|
||||
@@ -36,6 +39,11 @@ struct TransformComponent {
|
||||
scale = node->getScale();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark transform as changed (increment version)
|
||||
*/
|
||||
void markChanged() { version++; }
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_TRANSFORM_HPP
|
||||
|
||||
48
src/features/editScene/components/TriangleBuffer.hpp
Normal file
48
src/features/editScene/components/TriangleBuffer.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <ProceduralTriangleBuffer.h>
|
||||
|
||||
/**
|
||||
* @brief Component that holds a procedural triangle buffer
|
||||
*
|
||||
* The buffer contents are NOT serialized - only the configuration.
|
||||
* The buffer is rebuilt from Primitive children when needed.
|
||||
*/
|
||||
struct TriangleBufferComponent {
|
||||
// The actual triangle buffer (runtime only, not serialized)
|
||||
// Using shared_ptr because flecs needs to copy components
|
||||
std::shared_ptr<Procedural::TriangleBuffer> buffer;
|
||||
|
||||
// Reference to entity with ProceduralTexture for UV mapping
|
||||
flecs::entity textureEntity;
|
||||
|
||||
// Name of the rectangle in the texture atlas to use for UVs
|
||||
std::string textureRectName;
|
||||
|
||||
// Reference to entity with ProceduralMaterial for material
|
||||
flecs::entity materialEntity;
|
||||
|
||||
// Whether to attach to StaticGeometry instead of SceneNode
|
||||
bool useStaticGeometry = false;
|
||||
|
||||
// Reference to StaticGeometry region entity (if useStaticGeometry is true)
|
||||
flecs::entity staticGeometryEntity;
|
||||
|
||||
// Mesh name for the generated mesh
|
||||
std::string meshName;
|
||||
|
||||
// Whether the buffer needs rebuilding
|
||||
bool dirty = true;
|
||||
|
||||
// Whether the buffer has been converted to a mesh
|
||||
bool meshCreated = false;
|
||||
|
||||
// Pointer to the created Ogre entity (if not using StaticGeometry)
|
||||
Ogre::Entity* ogreEntity = nullptr;
|
||||
|
||||
void markDirty() { dirty = true; }
|
||||
};
|
||||
40
src/features/editScene/components/TriangleBufferModule.cpp
Normal file
40
src/features/editScene/components/TriangleBufferModule.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "TriangleBuffer.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/TriangleBufferEditor.hpp"
|
||||
#include <OgreMeshManager.h>
|
||||
|
||||
// Register TriangleBuffer component
|
||||
REGISTER_COMPONENT("Triangle Buffer", TriangleBufferComponent, TriangleBufferEditor)
|
||||
{
|
||||
registry.registerComponent<TriangleBufferComponent>(
|
||||
"Triangle Buffer",
|
||||
std::make_unique<TriangleBufferEditor>(sceneMgr),
|
||||
// Adder
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<TriangleBufferComponent>()) {
|
||||
e.set<TriangleBufferComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (e.has<TriangleBufferComponent>()) {
|
||||
auto& tb = e.get_mut<TriangleBufferComponent>();
|
||||
// Clean up Ogre entity and mesh
|
||||
if (tb.ogreEntity && sceneMgr) {
|
||||
try {
|
||||
sceneMgr->destroyEntity(tb.ogreEntity);
|
||||
} catch (...) {}
|
||||
tb.ogreEntity = nullptr;
|
||||
}
|
||||
if (!tb.meshName.empty()) {
|
||||
try {
|
||||
if (Ogre::MeshManager::getSingletonPtr()) {
|
||||
Ogre::MeshManager::getSingleton().remove(tb.meshName);
|
||||
}
|
||||
} catch (...) {}
|
||||
}
|
||||
e.remove<TriangleBufferComponent>();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
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
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "Gizmo.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/StaticGeometryMember.hpp"
|
||||
#include <cmath>
|
||||
|
||||
// Simple colors for axes - not using vertex colors since RTSS has issues
|
||||
@@ -40,34 +41,54 @@ Gizmo::Gizmo(Ogre::SceneManager *sceneMgr)
|
||||
m_gizmoNode->setVisible(false);
|
||||
}
|
||||
|
||||
Gizmo::~Gizmo()
|
||||
void Gizmo::shutdown()
|
||||
{
|
||||
if (!m_sceneMgr) {
|
||||
return; // Already shutdown
|
||||
}
|
||||
|
||||
// Detach objects from node first to avoid double-delete
|
||||
if (m_gizmoNode) {
|
||||
if (m_axisX) m_gizmoNode->detachObject(m_axisX);
|
||||
if (m_axisY) m_gizmoNode->detachObject(m_axisY);
|
||||
if (m_axisZ) m_gizmoNode->detachObject(m_axisZ);
|
||||
// Use try-catch since Ogre objects may be invalid during app shutdown
|
||||
if (m_gizmoNode && m_axisX) {
|
||||
try { m_gizmoNode->detachObject(m_axisX); } catch (...) {}
|
||||
}
|
||||
if (m_gizmoNode && m_axisY) {
|
||||
try { m_gizmoNode->detachObject(m_axisY); } catch (...) {}
|
||||
}
|
||||
if (m_gizmoNode && m_axisZ) {
|
||||
try { m_gizmoNode->detachObject(m_axisZ); } catch (...) {}
|
||||
}
|
||||
|
||||
// Now destroy the manual objects
|
||||
if (m_axisX) {
|
||||
m_sceneMgr->destroyManualObject(m_axisX);
|
||||
try { m_sceneMgr->destroyManualObject(m_axisX); } catch (...) {}
|
||||
m_axisX = nullptr;
|
||||
}
|
||||
if (m_axisY) {
|
||||
m_sceneMgr->destroyManualObject(m_axisY);
|
||||
try { m_sceneMgr->destroyManualObject(m_axisY); } catch (...) {}
|
||||
m_axisY = nullptr;
|
||||
}
|
||||
if (m_axisZ) {
|
||||
m_sceneMgr->destroyManualObject(m_axisZ);
|
||||
try { m_sceneMgr->destroyManualObject(m_axisZ); } catch (...) {}
|
||||
m_axisZ = nullptr;
|
||||
}
|
||||
|
||||
// Finally destroy the node
|
||||
if (m_gizmoNode) {
|
||||
m_sceneMgr->destroySceneNode(m_gizmoNode);
|
||||
try { m_sceneMgr->destroySceneNode(m_gizmoNode); } catch (...) {}
|
||||
m_gizmoNode = nullptr;
|
||||
}
|
||||
|
||||
// Mark scene manager as invalid
|
||||
m_sceneMgr = nullptr;
|
||||
}
|
||||
|
||||
Gizmo::~Gizmo()
|
||||
{
|
||||
// If shutdown wasn't called, do cleanup now
|
||||
if (m_sceneMgr) {
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
void Gizmo::attachTo(flecs::entity entity)
|
||||
@@ -261,6 +282,12 @@ bool Gizmo::onMouseMoved(const Ogre::Ray &mouseRay, const Ogre::Vector2 &mouseDe
|
||||
}
|
||||
|
||||
transform.applyToNode();
|
||||
transform.markChanged();
|
||||
|
||||
// Mark StaticGeometryMember dirty if present
|
||||
if (m_attachedEntity.has<StaticGeometryMemberComponent>()) {
|
||||
m_attachedEntity.get_mut<StaticGeometryMemberComponent>().markDirty();
|
||||
}
|
||||
|
||||
// Update gizmo to follow (use derived position)
|
||||
m_gizmoNode->setPosition(transform.node->_getDerivedPosition());
|
||||
|
||||
@@ -27,6 +27,11 @@ public:
|
||||
|
||||
Gizmo(Ogre::SceneManager *sceneMgr);
|
||||
~Gizmo();
|
||||
|
||||
/**
|
||||
* Shutdown and cleanup - must be called before SceneManager is destroyed
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
/**
|
||||
* Attach to an entity's transform
|
||||
|
||||
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
|
||||
-- =============================================================================
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user