Compare commits

...

93 Commits

Author SHA1 Message Date
11530dd7fc Dialogue uses arrays 2026-05-03 01:25:25 +03:00
3fd167ebff More events added 2026-05-03 01:11:14 +03:00
5952a96ee6 Fixed fonts sizes 2026-05-03 00:02:24 +03:00
76c3ead4a8 game_start event works 2026-05-02 23:43:48 +03:00
39a053d4ee Game mode API 2026-05-02 20:25:16 +03:00
c5da977857 Dualogue event API doc/samples 2026-05-02 18:25:30 +03:00
3e7b0169d5 Lua behavior tree 2026-05-01 13:54:44 +03:00
f918c5cefb Better handling of lua tasks 2026-05-01 04:02:47 +03:00
976ced3731 Lua-based behavior tree node 2026-05-01 00:31:06 +03:00
0fd8deaf53 direct save/load action database 2026-04-30 20:21:18 +03:00
4d843c18c7 Lua API 2026-04-30 19:07:35 +03:00
0ed83966da Lua action APIs 2026-04-30 10:03:56 +03:00
998984f75a Root motion fixed now 2026-04-29 18:45:37 +03:00
02fa78764a Lua API implemented 2026-04-29 14:13:50 +03:00
abe6eef6b3 Modules update 2026-04-29 12:53:13 +03:00
cca732b41b Fixes for event system 2026-04-27 20:53:04 +03:00
8507a3a501 Event system 2026-04-27 18:45:01 +03:00
b9cce0248a Labels and actuators work perfectly! 2026-04-27 09:03:47 +03:00
fa49bb5005 Actuators 2026-04-27 06:55:05 +03:00
37441aa8fd Motion fixed 2026-04-27 06:04:14 +03:00
a1b74aa2d5 Now Path Following component works 2026-04-27 05:37:41 +03:00
c80d9c96e6 AI motion refactoring 2026-04-27 05:24:45 +03:00
a75db85027 Can disable physics stepping 2026-04-26 22:15:15 +03:00
7563937ab8 Prefab placement at cursor 2026-04-26 20:51:20 +03:00
425bb8411d Fixed crash with entity destruction 2026-04-26 17:56:19 +03:00
9b29b68b33 Prefab editing and window hiding 2026-04-26 17:45:57 +03:00
7557c710fb Added prefabs 2026-04-26 16:43:37 +03:00
ce2f6c1306 Navmesh generation works with cell grids 2026-04-26 14:28:54 +03:00
e0e8e316d4 Fixed navmesh 2026-04-26 00:50:55 +03:00
abd2dc22d3 Now can test smart object action on player character again 2026-04-26 00:00:36 +03:00
a5df60769f Now repeated smart object action works perfectly 2026-04-25 23:08:00 +03:00
75ba39895f Teleport node works 2026-04-25 21:55:21 +03:00
2cff982473 Delays and animation conflicts fixed 2026-04-25 20:59:50 +03:00
3bd2801d1d Smart objects work! 2026-04-25 09:04:12 +03:00
2e358275f0 Path following works great 2026-04-25 01:17:31 +03:00
5ed7552164 Path following 2026-04-25 00:11:21 +03:00
2b3482da88 Normal display tool implemented 2026-04-24 21:12:02 +03:00
1d2c330481 Material fixes, playing with navmesh 2026-04-24 20:29:54 +03:00
a0d2561587 navmesh 2026-04-24 04:37:38 +03:00
e95b904f4e Underwater effect 2026-04-23 01:55:09 +03:00
9d4fad1d10 Water plane 2026-04-23 01:29:53 +03:00
4335a8cb05 Skybox and sun 2026-04-23 00:38:20 +03:00
d55bf970e0 Swim animations 2026-04-22 21:38:33 +03:00
30814ea35a Added animations for swimming 2026-04-22 19:50:14 +03:00
35f50f7f51 Fixed buoyancy 2026-04-22 18:57:33 +03:00
ca5b5b3052 Buoyancy 2026-04-22 17:27:40 +03:00
7e4e8f6638 Color atlas serialization fixes 2026-04-21 11:42:46 +03:00
c6fb3bb463 Camera works now 2026-04-21 03:59:45 +03:00
1411990def Forward/backwards fixed 2026-04-21 03:27:53 +03:00
1488d7d918 Camera change... 2026-04-21 03:04:47 +03:00
ef708fa14a Fixed game mode menu 2026-04-20 22:10:17 +03:00
6d7fcb1157 Not so well working game mode 2026-04-20 20:21:27 +03:00
4313d190f9 Characters are fully functional now 2026-04-20 12:23:31 +03:00
a2173114b9 Some questionable changes 2026-04-19 23:50:00 +03:00
fb6881998c Added character physics but it does not work yet 2026-04-19 23:45:00 +03:00
529476d8cd Animation Tree was implemented 2026-04-19 22:06:46 +03:00
43e9fb330f Character Animation 2026-04-19 19:24:55 +03:00
a392eb0bf9 Character display 2026-04-19 18:19:48 +03:00
e2960d67e4 Added character models and lua scripts to groups 2026-04-18 11:35:38 +03:00
79b6af1fff Furniture vertical offset 2026-04-16 17:11:12 +03:00
863c401230 De-clutter the scene 2026-04-16 05:39:56 +03:00
eec0d8f6f7 Physics works 2026-04-15 23:49:24 +03:00
c2a1db5a65 The furniture is batched now 2026-04-15 04:28:30 +03:00
77f93659d5 Furiture placement works 2026-04-14 21:44:06 +03:00
febeb8ff8d Furniture enabled 2026-04-14 18:54:28 +03:00
611dcd0d46 FPS/batch counter works 2026-04-14 13:55:39 +03:00
e6494936d6 Atlas margin setting support 2026-04-14 13:09:14 +03:00
e3b90e8bba Fixed room connectivity code 2026-04-14 12:35:20 +03:00
7846082220 valid connection doors positions 2026-04-13 11:51:26 +03:00
a955f0b218 Almost good generation of room layout 2026-04-13 04:03:48 +03:00
da4a1a6722 Now camera switching works! 2026-04-12 19:22:18 +03:00
21879c2784 Groups and components 2026-04-12 04:04:40 +03:00
5377d1a75a Material editing fixes 2026-04-12 01:36:58 +03:00
03f72bdd77 roof color editing 2026-04-11 14:59:43 +03:00
3c47a87768 Roof now works 2026-04-08 20:06:40 +03:00
4ba28fe512 Town components indicators 2026-04-07 11:37:43 +03:00
9f2f0be4a3 Adjusted frame geometry 2026-04-07 11:21:01 +03:00
7d64ba30cb CellGrid+Frames done 2026-04-07 01:39:32 +03:00
82c0e8c6ce Fixed doors and windows geometry 2026-04-07 01:18:48 +03:00
3798f227a7 Fixed external door geometry 2026-04-06 23:00:20 +03:00
19e4d80741 Proper walls geometry with frames 2026-04-06 19:31:10 +03:00
d8122e3275 Fixed floor offset 2026-04-06 02:16:45 +03:00
0ebba40867 Corners finally match 2026-04-06 01:45:26 +03:00
64b03abb48 Fixed material for Lot geometry and CellGrid is re-created now 2026-04-05 19:51:22 +03:00
cfd9dde5da Town plaza fixed 2026-04-05 17:23:28 +03:00
07101fcc64 Fixed crash on texture change 2026-04-04 05:36:08 +03:00
b8c61da1f7 TriangleBuffer 2026-04-04 05:13:15 +03:00
b1413d6d00 Procedural texture implementation 2026-04-04 03:42:50 +03:00
f785339852 Added OgreProcedural support 2026-04-04 02:21:18 +03:00
c2cbd0974d Added StaticGeometry support 2026-04-04 02:12:38 +03:00
9e72a48457 Fixed LOD support 2026-04-04 00:42:27 +03:00
ebd875feac Static body check 2026-04-03 21:12:45 +03:00
2a2fd53c4f Camera and Light components 2026-04-03 20:08:47 +03:00
544 changed files with 238086 additions and 6540 deletions

View File

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

View File

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

View File

@@ -69,3 +69,27 @@ material Debug/Red2
}
}
/**
* Debug material for normal visualization overlay.
* Renders on top of everything (depth_check off, depth_write off)
* Uses vertex colours so each normal line can have its own colour.
* Rendered in overlay queue to appear on top of all geometry.
*/
material Debug/NormalOverlay
{
technique
{
pass
{
lighting off
depth_check off
depth_write off
ambient 1.0 1.0 1.0 1.0
diffuse vertexcolour
specular 0.0 0.0 0.0 1.0
cull_software none
cull_hardware none
scene_blend alpha_blend
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,515 @@
#include "ActionDatabase.hpp"
#ifndef OGRE_STUB_H
#include <OgreLogManager.h>
#include <OgreResourceGroupManager.h>
#endif
#include <flecs.h>
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
// ---------------------------------------------------------------------------
// Singleton
// ---------------------------------------------------------------------------
ActionDatabase &ActionDatabase::getSingleton()
{
static ActionDatabase instance;
return instance;
}
ActionDatabase *ActionDatabase::getSingletonPtr()
{
return &getSingleton();
}
// ---------------------------------------------------------------------------
// Find methods
// ---------------------------------------------------------------------------
const GoapAction *ActionDatabase::findAction(const Ogre::String &name) const
{
for (const auto &action : actions) {
if (action.name == name)
return &action;
}
return nullptr;
}
GoapAction *ActionDatabase::findAction(const Ogre::String &name)
{
for (auto &action : actions) {
if (action.name == name)
return &action;
}
return nullptr;
}
const GoapGoal *ActionDatabase::findGoal(const Ogre::String &name) const
{
for (const auto &goal : goals) {
if (goal.name == name)
return &goal;
}
return nullptr;
}
GoapGoal *ActionDatabase::findGoal(const Ogre::String &name)
{
for (auto &goal : goals) {
if (goal.name == name)
return &goal;
}
return nullptr;
}
// ---------------------------------------------------------------------------
// Add or replace
// ---------------------------------------------------------------------------
void ActionDatabase::addOrReplaceAction(const GoapAction &action)
{
for (auto &a : actions) {
if (a.name == action.name) {
a = action;
return;
}
}
actions.push_back(action);
}
void ActionDatabase::addOrReplaceGoal(const GoapGoal &goal)
{
for (auto &g : goals) {
if (g.name == goal.name) {
g = goal;
return;
}
}
goals.push_back(goal);
}
// ---------------------------------------------------------------------------
// Remove methods
// ---------------------------------------------------------------------------
bool ActionDatabase::removeAction(const Ogre::String &name)
{
for (auto it = actions.begin(); it != actions.end(); ++it) {
if (it->name == name) {
actions.erase(it);
return true;
}
}
return false;
}
bool ActionDatabase::removeGoal(const Ogre::String &name)
{
for (auto it = goals.begin(); it != goals.end(); ++it) {
if (it->name == name) {
goals.erase(it);
return true;
}
}
return false;
}
// ---------------------------------------------------------------------------
// Selection / validation
// ---------------------------------------------------------------------------
const GoapGoal *
ActionDatabase::selectBestGoal(const GoapBlackboard &blackboard) const
{
const GoapGoal *best = nullptr;
int bestPriority = -1;
for (const auto &goal : goals) {
if (!goal.isValid(blackboard))
continue;
if (goal.priority > bestPriority) {
bestPriority = goal.priority;
best = &goal;
}
}
return best;
}
std::vector<const GoapAction *>
ActionDatabase::getValidActions(const GoapBlackboard &blackboard) const
{
std::vector<const GoapAction *> result;
for (const auto &action : actions) {
if (action.canRun(blackboard))
result.push_back(&action);
}
return result;
}
// ---------------------------------------------------------------------------
// Clear
// ---------------------------------------------------------------------------
void ActionDatabase::clear()
{
actions.clear();
goals.clear();
}
// ---------------------------------------------------------------------------
// ActionDatabaseComponent
// ---------------------------------------------------------------------------
void ActionDatabaseComponent::syncToSingleton() const
{
auto &db = ActionDatabase::getSingleton();
db.clear();
for (const auto &action : actions)
db.addOrReplaceAction(action);
for (const auto &goal : goals)
db.addOrReplaceGoal(goal);
}
// ---------------------------------------------------------------------------
// JSON serialization helpers (local to this translation unit)
// ---------------------------------------------------------------------------
static nlohmann::json serializeGoapBlackboard(const GoapBlackboard &bb)
{
nlohmann::json json;
json["bits"] = (uint64_t)bb.bits;
json["mask"] = (uint64_t)bb.mask;
if (bb.bitmask != ~0ULL)
json["bitmask"] = (uint64_t)bb.bitmask;
if (!bb.values.empty()) {
json["values"] = nlohmann::json::object();
for (const auto &pair : bb.values)
json["values"][pair.first] = pair.second;
}
if (!bb.floatValues.empty()) {
json["floatValues"] = nlohmann::json::object();
for (const auto &pair : bb.floatValues)
json["floatValues"][pair.first] = pair.second;
}
if (!bb.vec3Values.empty()) {
json["vec3Values"] = nlohmann::json::object();
for (const auto &pair : bb.vec3Values) {
nlohmann::json v;
v.push_back(pair.second.x);
v.push_back(pair.second.y);
v.push_back(pair.second.z);
json["vec3Values"][pair.first] = v;
}
}
return json;
}
static void deserializeGoapBlackboard(GoapBlackboard &bb,
const nlohmann::json &json)
{
bb.bits = json.value("bits", (uint64_t)0);
bb.mask = json.value("mask", (uint64_t)0);
bb.bitmask = json.value("bitmask", ~0ULL);
bb.values.clear();
if (json.contains("values") && json["values"].is_object()) {
for (auto &[key, val] : json["values"].items())
bb.values[key] = val.get<int>();
}
bb.floatValues.clear();
if (json.contains("floatValues") && json["floatValues"].is_object()) {
for (auto &[key, val] : json["floatValues"].items())
bb.floatValues[key] = val.get<float>();
}
bb.vec3Values.clear();
if (json.contains("vec3Values") && json["vec3Values"].is_object()) {
for (auto &[key, val] : json["vec3Values"].items()) {
if (val.is_array() && val.size() >= 3)
bb.vec3Values[key] =
Ogre::Vector3(val[0].get<float>(),
val[1].get<float>(),
val[2].get<float>());
}
}
}
static nlohmann::json serializeBehaviorTreeNode(const BehaviorTreeNode &node)
{
nlohmann::json json;
json["type"] = node.type;
if (!node.name.empty())
json["name"] = node.name;
if (!node.params.empty())
json["params"] = node.params;
if (!node.children.empty()) {
json["children"] = nlohmann::json::array();
for (const auto &child : node.children)
json["children"].push_back(
serializeBehaviorTreeNode(child));
}
return json;
}
static void deserializeBehaviorTreeNode(BehaviorTreeNode &node,
const nlohmann::json &json)
{
node.type = json.value("type", "task");
node.name = json.value("name", "");
node.params = json.value("params", "");
node.children.clear();
if (json.contains("children") && json["children"].is_array()) {
for (const auto &childJson : json["children"]) {
BehaviorTreeNode child;
deserializeBehaviorTreeNode(child, childJson);
node.children.push_back(child);
}
}
}
static nlohmann::json serializeGoapAction(const GoapAction &action)
{
nlohmann::json json;
json["name"] = action.name;
json["cost"] = action.cost;
json["preconditions"] = serializeGoapBlackboard(action.preconditions);
json["effects"] = serializeGoapBlackboard(action.effects);
if (action.preconditionMask != ~0ULL)
json["preconditionMask"] = action.preconditionMask;
json["behaviorTree"] = serializeBehaviorTreeNode(action.behaviorTree);
if (!action.behaviorTreeName.empty())
json["behaviorTreeName"] = action.behaviorTreeName;
return json;
}
static void deserializeGoapAction(GoapAction &action,
const nlohmann::json &json)
{
action.name = json.value("name", "Unnamed");
action.cost = json.value("cost", 1);
if (json.contains("preconditions"))
deserializeGoapBlackboard(action.preconditions,
json["preconditions"]);
if (json.contains("effects"))
deserializeGoapBlackboard(action.effects, json["effects"]);
action.preconditionMask = json.value("preconditionMask", ~0ULL);
if (json.contains("behaviorTree"))
deserializeBehaviorTreeNode(action.behaviorTree,
json["behaviorTree"]);
action.behaviorTreeName = json.value("behaviorTreeName", "");
}
static nlohmann::json serializeGoapGoal(const GoapGoal &goal)
{
nlohmann::json json;
json["name"] = goal.name;
json["priority"] = goal.priority;
json["target"] = serializeGoapBlackboard(goal.target);
if (!goal.condition.empty())
json["condition"] = goal.condition;
return json;
}
static void deserializeGoapGoal(GoapGoal &goal, const nlohmann::json &json)
{
goal.name = json.value("name", "Unnamed");
goal.priority = json.value("priority", 1);
if (json.contains("target"))
deserializeGoapBlackboard(goal.target, json["target"]);
goal.condition = json.value("condition", "");
}
// ---------------------------------------------------------------------------
// saveToJson / loadFromJson
// ---------------------------------------------------------------------------
bool ActionDatabase::saveToJson(const std::string &filename)
{
try {
// Resolve the filesystem path from the "General" resource group
Ogre::ResourceGroupManager &rgm =
Ogre::ResourceGroupManager::getSingleton();
const Ogre::ResourceGroupManager::LocationList &locations =
rgm.getResourceLocationList("General");
if (locations.empty()) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::saveToJson: "
"no resource locations for group 'General'");
return false;
}
// Use the first location's path
std::string dir = locations.begin()->archive->getName();
std::string filepath = dir + "/" + filename;
// Backup existing file
if (std::filesystem::exists(filepath)) {
std::string backup = filepath + ".bak";
try {
std::filesystem::copy_file(
filepath, backup,
std::filesystem::copy_options::
overwrite_existing);
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::saveToJson: "
"backup failed: " +
Ogre::String(e.what()));
}
}
const ActionDatabase &db = getSingleton();
nlohmann::json root;
root["actions"] = nlohmann::json::array();
for (const auto &action : db.actions)
root["actions"].push_back(serializeGoapAction(action));
root["goals"] = nlohmann::json::array();
for (const auto &goal : db.goals)
root["goals"].push_back(serializeGoapGoal(goal));
// Save bit names
nlohmann::json bitNames = nlohmann::json::array();
for (int i = 0; i < 64; i++) {
const char *name = GoapBlackboard::getBitName(i);
if (name) {
nlohmann::json entry;
entry["index"] = i;
entry["name"] = name;
bitNames.push_back(entry);
}
}
if (!bitNames.empty())
root["bitNames"] = bitNames;
std::ofstream file(filepath);
if (!file.is_open()) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::saveToJson: "
"failed to open " +
filepath);
return false;
}
file << root.dump(4);
file.close();
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase saved to " + filepath);
return true;
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::saveToJson error: " +
Ogre::String(e.what()));
return false;
}
}
bool ActionDatabase::loadFromJson(const std::string &filename)
{
try {
// Resolve the filesystem path from the "General" resource group
Ogre::ResourceGroupManager &rgm =
Ogre::ResourceGroupManager::getSingleton();
const Ogre::ResourceGroupManager::LocationList &locations =
rgm.getResourceLocationList("General");
if (locations.empty()) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::loadFromJson: "
"no resource locations for group 'General'");
return false;
}
std::string dir = locations.begin()->archive->getName();
std::string filepath = dir + "/" + filename;
if (!std::filesystem::exists(filepath)) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::loadFromJson: "
"file not found: " +
filepath);
return false;
}
std::ifstream file(filepath);
if (!file.is_open()) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::loadFromJson: "
"failed to open " +
filepath);
return false;
}
nlohmann::json root;
try {
file >> root;
} catch (const nlohmann::json::parse_error &e) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::loadFromJson: "
"JSON parse error in " +
filepath + ": " + Ogre::String(e.what()));
return false;
}
file.close();
ActionDatabase &db = getSingleton();
// Load actions (add/replace)
if (root.contains("actions") && root["actions"].is_array()) {
for (const auto &actionJson : root["actions"]) {
GoapAction action;
deserializeGoapAction(action, actionJson);
db.addOrReplaceAction(action);
}
}
// Load goals (add/replace)
if (root.contains("goals") && root["goals"].is_array()) {
for (const auto &goalJson : root["goals"]) {
GoapGoal goal;
deserializeGoapGoal(goal, goalJson);
db.addOrReplaceGoal(goal);
}
}
// Load bit names
if (root.contains("bitNames") && root["bitNames"].is_array()) {
for (const auto &entry : root["bitNames"]) {
if (entry.contains("index") &&
entry.contains("name"))
GoapBlackboard::setBitName(
entry["index"].get<int>(),
entry["name"]
.get<std::string>());
}
}
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase loaded from " + filepath);
return true;
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::loadFromJson error: " +
Ogre::String(e.what()));
return false;
}
}
// ---------------------------------------------------------------------------
// reloadFromSceneComponents
// ---------------------------------------------------------------------------
void ActionDatabase::reloadFromSceneComponents(flecs::world &world)
{
// First, load from file (if available) — this is done by the caller
// before calling this function. Here we just re-sync from scene
// entities so that scene-defined actions are applied on top.
// Iterate all entities with ActionDatabaseComponent
world.each([](flecs::entity e, ActionDatabaseComponent &dbComp) {
(void)e;
dbComp.syncToSingleton();
});
}

View File

@@ -0,0 +1,114 @@
#ifndef EDITSCENE_ACTION_DATABASE_HPP
#define EDITSCENE_ACTION_DATABASE_HPP
#pragma once
#include "GoapAction.hpp"
#include "GoapGoal.hpp"
#include <vector>
#include <unordered_map>
#include <string>
// Forward declaration for reloadFromSceneComponents
namespace flecs
{
class world;
}
/**
* Global action database singleton.
*
* Holds the master list of GOAP actions and goals that characters can use.
* This is a singleton accessible from anywhere in the codebase.
* The ActionDatabaseComponent on a scene entity stores the actions/goals
* and syncs them to the singleton on scene load.
*/
class ActionDatabase {
public:
/** Get the singleton instance */
static ActionDatabase &getSingleton();
static ActionDatabase *getSingletonPtr();
std::vector<GoapAction> actions;
std::vector<GoapGoal> goals;
// Find an action by name
const GoapAction *findAction(const Ogre::String &name) const;
GoapAction *findAction(const Ogre::String &name);
// Find a goal by name
const GoapGoal *findGoal(const Ogre::String &name) const;
GoapGoal *findGoal(const Ogre::String &name);
// Add or replace an action by name
void addOrReplaceAction(const GoapAction &action);
// Add or replace a goal by name
void addOrReplaceGoal(const GoapGoal &goal);
// Remove an action by name
bool removeAction(const Ogre::String &name);
// Remove a goal by name
bool removeGoal(const Ogre::String &name);
// Select the best valid goal for a given blackboard
// Returns nullptr if no valid goal exists
const GoapGoal *selectBestGoal(const GoapBlackboard &blackboard) const;
// Build a list of actions that can run from a given blackboard state
std::vector<const GoapAction *>
getValidActions(const GoapBlackboard &blackboard) const;
// Clear all actions and goals
void clear();
/**
* Save the action database to a JSON file.
* Creates a backup of the existing file (if any) by appending ".bak".
* The file is written to the filesystem path resolved from the
* "General" resource group.
*
* @param filename The filename (e.g. "actions.json").
* @return true on success.
*/
static bool saveToJson(const std::string &filename);
/**
* Load the action database from a JSON file.
* The file is located via the "General" resource group.
* On failure (file not found, parse error) the error is logged
* and the database is left unchanged.
*
* @param filename The filename (e.g. "actions.json").
* @return true on success.
*/
static bool loadFromJson(const std::string &filename);
/**
* Re-process all ActionDatabaseComponent entities in the given
* Flecs world: clear the singleton and re-sync from every entity
* that carries the component. This is used after a reload so
* scene-defined actions are re-applied on top of the file.
*/
static void reloadFromSceneComponents(flecs::world &world);
private:
ActionDatabase() = default;
~ActionDatabase() = default;
ActionDatabase(const ActionDatabase &) = delete;
ActionDatabase &operator=(const ActionDatabase &) = delete;
};
/**
* Flecs component that stores action database data on a scene entity.
* When set on an entity, it syncs its contents to the ActionDatabase singleton.
*/
struct ActionDatabaseComponent {
std::vector<GoapAction> actions;
std::vector<GoapGoal> goals;
/** Sync this component's data to the ActionDatabase singleton */
void syncToSingleton() const;
};
#endif // EDITSCENE_ACTION_DATABASE_HPP

View File

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

View File

@@ -0,0 +1,39 @@
#ifndef EDITSCENE_ACTION_DEBUG_HPP
#define EDITSCENE_ACTION_DEBUG_HPP
#pragma once
#include "GoapBlackboard.hpp"
#include <Ogre.h>
#include <vector>
#include <unordered_map>
/**
* Per-character action debug component.
*
* Allows test-running individual actions and inspecting the character's
* local blackboard state. Used for debugging AI behavior in the editor.
*
* Path following animation states have been moved to PathFollowingComponent.
*/
struct ActionDebug {
// Character's local GOAP blackboard
GoapBlackboard blackboard;
// Currently selected action for test-running
Ogre::String selectedActionName;
// Currently selected goal for testing
Ogre::String selectedGoalName;
// Test-run state
bool isRunning = false;
float runTimer = 0.0f;
Ogre::String currentActionName;
// Debug output
Ogre::String lastResult;
ActionDebug() = default;
};
#endif // EDITSCENE_ACTION_DEBUG_HPP

View File

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

View File

@@ -0,0 +1,44 @@
#ifndef EDITSCENE_ACTUATOR_HPP
#define EDITSCENE_ACTUATOR_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <string>
/**
* Actuator component.
*
* An interactive object visible only to the player character.
* When the player is within radius + height range, an on-screen
* prompt appears and the action can be triggered with the action key.
*
* Unlike SmartObject, Actuators do not use pathfinding or path
* following — they are instantaneous interactions.
*/
struct ActuatorComponent {
// Interaction radius in XZ plane
float radius = 1.5f;
// Maximum height difference for interaction
float height = 1.8f;
// Names of GOAP actions (from ActionDatabase) that this actuator provides
std::vector<Ogre::String> actionNames;
// Runtime: cooldown timer (seconds remaining)
float cooldownTimer = 0.0f;
// Runtime: currently executing an action
bool isExecuting = false;
ActuatorComponent() = default;
explicit ActuatorComponent(float radius_, float height_)
: radius(radius_)
, height(height_)
{
}
};
#endif // EDITSCENE_ACTUATOR_HPP

View File

@@ -0,0 +1,19 @@
#include "Actuator.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/ActuatorEditor.hpp"
REGISTER_COMPONENT_GROUP("Actuator", "Game", ActuatorComponent, ActuatorEditor)
{
registry.registerComponent<ActuatorComponent>(
"Actuator", "Game", std::make_unique<ActuatorEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<ActuatorComponent>())
e.set<ActuatorComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<ActuatorComponent>())
e.remove<ActuatorComponent>();
});
}

View File

@@ -0,0 +1,9 @@
#include "AnimationTree.hpp"
AnimationTreeComponent::AnimationTreeComponent()
: root()
, enabled(true)
, useRootMotion(false)
, dirty(true)
{
}

View 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

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

View File

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

View File

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

View File

@@ -0,0 +1,130 @@
#ifndef EDITSCENE_BEHAVIOR_TREE_HPP
#define EDITSCENE_BEHAVIOR_TREE_HPP
#pragma once
#include <Ogre.h>
#include <vector>
/**
* Data-driven behavior tree node for AI action execution.
*
* Node types:
* "sequence" - Execute children in order until one fails
* "selector" - Execute children in order until one succeeds
* "invert" - Invert the result of a single child
* "task" - Leaf action (references a named task)
* "check" - Leaf condition (references a named check)
* "debugPrint" - Leaf: prints 'name' to console once when active
* "setAnimationState"- Leaf: sets animation state (name="SM/State")
* "isAnimationEnded" - Leaf check: true if anim in state machine ended
* "setBit" - Leaf: sets blackboard bit (name=bit, params=0/1)
* "checkBit" - Leaf check: true if blackboard bit is set
* "setValue" - Leaf: sets blackboard value (name=key, params=val)
* "checkValue" - Leaf check: blackboard comparison (name=key, params="op val")
* "blackboardDump" - Leaf: dumps entire blackboard to log
* "delay" - Leaf: waits for N seconds (params=seconds as float)
* "teleportToChild" - Leaf: teleports character to a named child entity
* of the Smart Object being interacted with.
* name = child entity name to teleport to.
* The character is positioned at the child's absolute
* world transform (position + orientation).
* "disablePhysics" - Leaf: removes character's JPH::BodyID from physics
* system so physics no longer interferes with animation.
* "enablePhysics" - Leaf: re-adds character's JPH::BodyID to physics
* system to restore physics simulation.
*
* --- Item / Inventory nodes ---
* "hasItem" - Leaf check: true if character's inventory has an item
* matching the given itemId (name=itemId).
* "hasItemByName" - Leaf check: true if character's inventory has an item
* matching the given itemName (name=itemName).
* "countItem" - Leaf check: true if character's inventory has at least
* N of itemId (name=itemId, params=count as int).
* "pickupItem" - Leaf: picks up the nearest ItemComponent entity within
* range into the character's inventory.
* name=itemId filter (optional, empty = any).
* "dropItem" - Leaf: drops an item from inventory into the world.
* name=itemId, params=count (optional, default 1).
* "useItem" - Leaf: uses an item from inventory (executes its
* useAction behavior tree). name=itemId.
* "addItemToInventory"- Leaf: adds an item directly to character's inventory
* (for quest rewards, etc.).
* params="itemId,itemName,itemType,count,weight,value"
*
* --- Lua node ---
* "luaTask" - Leaf: calls a registered Lua function.
* name = registered node handler name.
* params = "key=val,key2=val2" passed to the Lua function.
* The Lua function receives (entity_id, params_table)
* and must return "success", "failure", or "running".
* Register handlers via:
* ecs.behavior_tree.register_node("name", function)
*/
struct BehaviorTreeNode {
Ogre::String type = "task";
Ogre::String name; // Action/condition name, or message, or SM/State
Ogre::String params; // Optional extra parameters
std::vector<BehaviorTreeNode> children;
BehaviorTreeNode() = default;
BehaviorTreeNode *findChild(const Ogre::String &childName)
{
for (auto &child : children) {
if (child.name == childName)
return &child;
}
return nullptr;
}
const BehaviorTreeNode *findChild(const Ogre::String &childName) const
{
for (const auto &child : children) {
if (child.name == childName)
return &child;
}
return nullptr;
}
bool canHaveChildren() const
{
return type == "sequence" || type == "selector" ||
type == "invert";
}
bool isLeaf() const
{
return type == "task" || type == "check" ||
type == "debugPrint" || type == "setAnimationState" ||
type == "isAnimationEnded" || type == "setBit" ||
type == "checkBit" || type == "setValue" ||
type == "checkValue" || type == "blackboardDump" ||
type == "delay" || type == "teleportToChild" ||
type == "disablePhysics" || type == "enablePhysics" ||
type == "sendEvent" || type == "hasItem" ||
type == "hasItemByName" || type == "countItem" ||
type == "pickupItem" || type == "dropItem" ||
type == "useItem" || type == "addItemToInventory" ||
type == "luaTask";
}
};
/**
* Behavior tree asset component.
*
* Can be attached to an entity to define a reusable behavior tree,
* or referenced by name from a GoapAction.
*/
struct BehaviorTreeComponent {
BehaviorTreeNode root;
Ogre::String treeName;
bool enabled = true;
bool dirty = true;
void markDirty()
{
dirty = true;
}
};
#endif // EDITSCENE_BEHAVIOR_TREE_HPP

View File

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

View File

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

View File

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

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

View 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-" },
};
}

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

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

View 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

View File

@@ -0,0 +1,7 @@
#pragma once
#include <flecs.h>
namespace CellGridModule {
void registerComponents(flecs::world& world);
}

View File

@@ -0,0 +1,61 @@
#ifndef EDITSCENE_CHARACTER_HPP
#define EDITSCENE_CHARACTER_HPP
#pragma once
#include <Ogre.h>
/**
* Character physics component
*
* Attaches a Jolt JPH::Character (kinematic capsule) to the entity.
* The entity may also have CharacterSlotsComponent; the character
* physics lives on the same entity as the visual character.
*
* Child entities can add extra collision shapes via PhysicsColliderComponent.
*/
struct CharacterComponent {
/* Capsule dimensions */
float radius = 0.3f;
float height = 1.8f; /* cylinder height (excluding spherical caps) */
/* Offset from the entity's scene node */
Ogre::Vector3 offset = Ogre::Vector3::ZERO;
/* Current linear velocity (m/s), applied each frame by CharacterSystem */
Ogre::Vector3 linearVelocity = Ogre::Vector3::ZERO;
/* Enable/disable physics character */
bool enabled = true;
/* Physics was explicitly disabled (e.g. by behavior tree node).
* When true, the character's JPH::BodyID is removed from the physics
* system but the JPH::Character object is kept alive so it can be
* re-added later. This is separate from 'enabled' which controls
* whether the character system processes this entity at all. */
bool physicsDisabled = false;
/* Dirty flag — triggers rebuild of the Jolt character */
bool dirty = true;
/* When true, the scene node position is driven by root motion
* (AnimationTreeSystem), not by physics. The physics character
* position is synced to match the scene node each frame, and
* physics does NOT write its position back to the scene node. */
bool useRootMotion = false;
/* Floor detection: raycast downward to find ground before enabling gravity */
bool hasFloor = false;
float floorCheckDistance = 2.0f;
bool useGravity = true;
float getHalfHeight() const
{
return height * 0.5f;
}
float getTotalHeight() const
{
return height + 2.0f * radius;
}
};
#endif // EDITSCENE_CHARACTER_HPP

View File

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

View File

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

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

View File

@@ -0,0 +1,131 @@
#ifndef EDITSCENE_DIALOGUE_COMPONENT_HPP
#define EDITSCENE_DIALOGUE_COMPONENT_HPP
#pragma once
#include <Ogre.h>
#include <functional>
#include <string>
#include <vector>
/**
* Visual-novel style dialogue box component.
*
* Displays a narration text box at the bottom of the screen with optional
* player choices. The dialogue can be driven via the EventBus system
* (using "dialogue_show" event) or directly via the component API.
*
* Only active in game mode (GamePlayState::Playing).
*
* Event payload (EventParams) parameters:
* "text" (string) - Narration text to display
* "choices" (string_array) - Array of choice label strings (Lua table)
* "speaker" (string) - Optional speaker name
* "auto_progress" (int) - If 1, clicking anywhere progresses (no choices)
*
* Component state transitions:
* Idle -> Showing (on show() or event)
* Showing -> AwaitingChoice (if choices provided)
* Showing -> Idle (if no choices, on click progress)
* AwaitingChoice -> Idle (on choice selected)
*/
struct DialogueComponent {
/** Current state of the dialogue box */
enum class State {
Idle, ///< No dialogue active
Showing, ///< Text is being displayed
AwaitingChoice ///< Waiting for player to pick a choice
};
State state = State::Idle;
/** The narration text to display */
Ogre::String text;
/** Optional speaker name (displayed above the text) */
Ogre::String speaker;
/** Player choice labels (empty = no choices, click to progress) */
std::vector<Ogre::String> choices;
/** Font configuration */
Ogre::String fontName = "Jupiteroid-Regular.ttf";
float fontSize = 24.0f;
/** Speaker name font size (slightly smaller) */
float speakerFontSize = 20.0f;
/** Background opacity (0.0 - 1.0) */
float backgroundOpacity = 0.85f;
/** Height of the dialogue box as fraction of screen height (0.0 - 1.0) */
float boxHeightFraction = 0.25f;
/** Vertical position as fraction from top (0.0 = top, 0.75 = bottom quarter) */
float boxPositionFraction = 0.75f;
/** Whether the dialogue box is enabled (can be toggled) */
bool enabled = true;
/** Callback invoked when a choice is selected (choice index, 1-based) */
std::function<void(int)> onChoiceSelected;
/** Callback invoked when dialogue is dismissed (no choices mode) */
std::function<void()> onDismissed;
/** Callback invoked when dialogue starts showing */
std::function<void()> onShow;
/* --- API --- */
/** Show dialogue with given text and optional choices */
void show(const Ogre::String &narrationText,
const std::vector<Ogre::String> &choiceLabels = {},
const Ogre::String &speakerName = "")
{
text = narrationText;
choices = choiceLabels;
speaker = speakerName;
state = choices.empty() ? State::Showing :
State::AwaitingChoice;
if (onShow)
onShow();
}
/** Progress the dialogue (click-through when no choices) */
void progress()
{
if (state == State::Showing && choices.empty()) {
state = State::Idle;
if (onDismissed)
onDismissed();
}
}
/** Select a choice by 1-based index */
void selectChoice(int index)
{
if (state == State::AwaitingChoice && index >= 1 &&
index <= (int)choices.size()) {
state = State::Idle;
if (onChoiceSelected)
onChoiceSelected(index);
}
}
/** Check if dialogue is currently active */
bool isActive() const
{
return state != State::Idle;
}
/** Reset dialogue to idle state */
void reset()
{
state = State::Idle;
text.clear();
choices.clear();
speaker.clear();
}
};
#endif // EDITSCENE_DIALOGUE_COMPONENT_HPP

View File

@@ -0,0 +1,23 @@
#include "../ui/ComponentRegistration.hpp"
#include "../ui/DialogueEditor.hpp"
#include "DialogueComponent.hpp"
REGISTER_COMPONENT_GROUP("Dialogue Box", "Game", DialogueComponent,
DialogueEditor)
{
registry.registerComponent<DialogueComponent>(
DialogueComponent_name, DialogueComponent_group,
std::make_unique<DialogueEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<DialogueComponent>()) {
e.set<DialogueComponent>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<DialogueComponent>()) {
e.remove<DialogueComponent>();
}
});
}

View File

@@ -0,0 +1,21 @@
#ifndef EDITSCENE_EVENT_HANDLER_HPP
#define EDITSCENE_EVENT_HANDLER_HPP
#pragma once
#include <Ogre.h>
/**
* Event-driven behavior tree handler component.
*
* When the specified event is received, the referenced GoapAction's
* behavior tree is executed for this entity. Event parameters
* (EventParams) are injected into the entity's GoapBlackboard before
* the tree runs and cleaned up when the tree completes.
*/
struct EventHandlerComponent {
Ogre::String eventName;
Ogre::String actionName;
bool enabled = true;
};
#endif // EDITSCENE_EVENT_HANDLER_HPP

View File

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

View File

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

View File

@@ -0,0 +1,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

View File

@@ -0,0 +1,74 @@
#ifndef EDITSCENE_GOAP_ACTION_HPP
#define EDITSCENE_GOAP_ACTION_HPP
#pragma once
#include "GoapBlackboard.hpp"
#include "BehaviorTree.hpp"
#include <Ogre.h>
/**
* A GOAP action definition.
*
* Actions live in the ActionDatabase and can be executed by any character.
* Each action has preconditions (required blackboard state),
* effects (resulting blackboard state), a cost, and a behavior tree.
*/
struct GoapAction {
Ogre::String name;
int cost = 1;
// GOAP preconditions and effects
GoapBlackboard preconditions;
GoapBlackboard effects;
// Bitmask for precondition checking. Only bits set here are compared.
// Defaults to all 1s (check all bits).
uint64_t preconditionMask = ~0ULL;
// Behavior tree to execute when this action is selected
BehaviorTreeNode behaviorTree;
// Optional: reference to a named behavior tree asset
Ogre::String behaviorTreeName;
GoapAction() = default;
explicit GoapAction(const Ogre::String &name_, int cost_ = 1)
: name(name_)
, cost(cost_)
{
}
// Check if the given blackboard satisfies this action's preconditions.
// Only bits in preconditionMask are compared.
bool canRun(const GoapBlackboard &blackboard) const
{
// Fast-path: if mask covers all set bits, use standard check
if (preconditionMask == ~0ULL)
return blackboard.satisfies(preconditions);
// Masked check: only compare bits in preconditionMask
uint64_t relevantBits = preconditionMask;
uint64_t bbBits = blackboard.bits & relevantBits;
uint64_t preBits = preconditions.bits & relevantBits;
uint64_t bbMask = blackboard.mask & relevantBits;
uint64_t preMask = preconditions.mask & relevantBits;
// All precondition bits must be present in blackboard
if ((bbMask & preMask) != preMask)
return false;
if ((bbBits & preMask) != preBits)
return false;
// Check integer values
for (const auto &kv : preconditions.values) {
if (!blackboard.hasValue(kv.first))
return false;
if (blackboard.getValue(kv.first) != kv.second)
return false;
}
return true;
}
};
#endif // EDITSCENE_GOAP_ACTION_HPP

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,99 @@
#ifndef EDITSCENE_GOAP_PLANNER_HPP
#define EDITSCENE_GOAP_PLANNER_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <string>
#include <queue>
/**
* GOAP Planner component.
*
* Holds a curated list of action and goal names from an ActionDatabase,
* plus configuration for smart-object action discovery.
* The planner resolves names against an ActionDatabase at runtime.
*
* The actionNames and goalNames lists act as external references:
* prefabs can store them even when the ActionDatabase is not
* part of the prefab itself.
*/
struct GoapPlannerComponent {
// Selected action names from ActionDatabase
std::vector<Ogre::String> actionNames;
// Selected goal names from ActionDatabase
std::vector<Ogre::String> goalNames;
// Maximum distance to search for smart objects with matching actions
float smartObjectDistance = 50.0f;
// Whether to include smart object actions in planning
bool includeSmartObjects = true;
// Optional reference to an external ActionDatabase entity by name.
Ogre::String actionDatabaseRef;
// -----------------------------------------------------------------
// Runtime plan queue (not serialized)
// -----------------------------------------------------------------
struct Plan {
std::vector<Ogre::String> actions;
int totalCost = 0;
Ogre::String goalName;
};
std::vector<Plan> planQueue;
// Planner status
enum class Status {
Idle, // No planning requested
Planning, // Currently planning
PlansAvailable, // One or more plans in queue
NoPlanFound // Planning finished but no valid plan found
};
Status status = Status::Idle;
// Goal name that was used for the current plan batch
Ogre::String currentGoalName;
// Planning control
bool planDirty = true;
int maxPlans = 3; // stop after generating this many plans
// Planning progress (for status display)
int plansGenerated = 0;
int nodesExplored = 0;
GoapPlannerComponent() = default;
void clearPlans()
{
planQueue.clear();
plansGenerated = 0;
status = Status::Idle;
}
// Pop the cheapest plan from the queue
Plan popCheapestPlan()
{
if (planQueue.empty())
return Plan();
size_t bestIdx = 0;
for (size_t i = 1; i < planQueue.size(); i++) {
if (planQueue[i].totalCost < planQueue[bestIdx].totalCost)
bestIdx = i;
}
Plan result = std::move(planQueue[bestIdx]);
planQueue.erase(planQueue.begin() + bestIdx);
if (planQueue.empty() && status == Status::PlansAvailable)
status = Status::Idle;
return result;
}
bool hasPlans() const
{
return !planQueue.empty();
}
};
#endif // EDITSCENE_GOAP_PLANNER_HPP

View File

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

View File

@@ -0,0 +1,50 @@
#ifndef EDITSCENE_GOAP_RUNNER_HPP
#define EDITSCENE_GOAP_RUNNER_HPP
#pragma once
#include <Ogre.h>
#include <string>
#include <vector>
/**
* GOAP Runner component.
*
* Executes plans from GoapPlannerComponent.
* For normal actions: runs the action's behavior tree.
* For smart object actions: pathfinds to the smart object and executes.
* After plan completion, marks the planner dirty for replanning.
*/
struct GoapRunnerComponent {
// Current plan execution state
enum class State {
Idle, // No plan running
RunningAction, // Executing a normal action
MovingToSmartObject, // Pathfinding to a smart object
ExecutingSmartObject, // Executing smart object action
PlanComplete // Plan finished, waiting for replan
};
State state = State::Idle;
// Index of current action in the plan
int currentActionIndex = 0;
// Name of the currently executing action
Ogre::String currentActionName;
// Timer for action execution
float actionTimer = 0.0f;
// Entity ID of target smart object (if applicable)
uint64_t targetSmartObjectId = 0;
// Active plan actions (copied from planner when plan starts)
std::vector<Ogre::String> planActions;
// Whether to auto-replan after completion
bool autoReplan = true;
GoapRunnerComponent() = default;
};
#endif // EDITSCENE_GOAP_RUNNER_HPP

View File

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

View File

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

View File

@@ -0,0 +1,165 @@
#ifndef EDITSCENE_INVENTORY_HPP
#define EDITSCENE_INVENTORY_HPP
#pragma once
#include <Ogre.h>
#include <flecs.h>
#include <vector>
#include <string>
#include <cstdint>
/**
* A single slot in an inventory.
* Stores a reference to an item entity (if the item is a world entity)
* or stores item data directly for items that exist only in inventory.
*/
struct InventorySlot {
// Flecs entity ID of the item (0 if slot is empty)
flecs::entity_t itemEntity = 0;
// Item data for items that exist only in inventory (no world entity)
Ogre::String itemId;
Ogre::String itemName;
Ogre::String itemType;
int stackSize = 0;
int maxStackSize = 99;
float weight = 0.1f;
int value = 1;
Ogre::String useActionName;
bool isEmpty() const
{
return itemEntity == 0 && stackSize <= 0;
}
void clear()
{
itemEntity = 0;
itemId.clear();
itemName.clear();
itemType.clear();
stackSize = 0;
maxStackSize = 99;
weight = 0.1f;
value = 1;
useActionName.clear();
}
};
/**
* Inventory component.
*
* Attached to a character entity to hold items.
* Can also be attached to container entities (chests, barrels, etc.)
* to define their contents.
*
* The inventory stores items as InventorySlot entries, each of which
* may reference a world ItemComponent entity or hold item data directly.
*/
struct InventoryComponent {
// Maximum number of slots
int maxSlots = 20;
// Current slots
std::vector<InventorySlot> slots;
// Total weight of all items (computed)
float totalWeight = 0.0f;
// Maximum weight capacity (0 = unlimited)
float maxWeight = 50.0f;
// Whether this inventory is a container (chest, barrel, etc.)
// Containers can be opened by characters to transfer items.
bool isContainer = false;
// Whether this inventory is currently open (for containers being browsed)
bool isOpen = false;
InventoryComponent() = default;
explicit InventoryComponent(int maxSlots_)
: maxSlots(maxSlots_)
{
slots.reserve(maxSlots_);
}
/** Find the first empty slot index, or -1 if full. */
int findEmptySlot() const
{
for (int i = 0; i < maxSlots; i++) {
if (i >= (int)slots.size())
return i;
if (slots[i].isEmpty())
return i;
}
return -1;
}
/** Find a slot containing an item with the given itemId. */
int findItem(const Ogre::String &itemId) const
{
for (int i = 0; i < (int)slots.size(); i++) {
if (!slots[i].isEmpty() && slots[i].itemId == itemId)
return i;
}
return -1;
}
/** Find a slot containing an item with the given itemName. */
int findItemByName(const Ogre::String &itemName) const
{
for (int i = 0; i < (int)slots.size(); i++) {
if (!slots[i].isEmpty() &&
slots[i].itemName == itemName)
return i;
}
return -1;
}
/** Count total number of items (sum of stack sizes). */
int countItems() const
{
int count = 0;
for (const auto &slot : slots) {
if (!slot.isEmpty())
count += slot.stackSize;
}
return count;
}
/** Count how many of a specific itemId are in the inventory. */
int countItem(const Ogre::String &itemId) const
{
int count = 0;
for (const auto &slot : slots) {
if (!slot.isEmpty() && slot.itemId == itemId)
count += slot.stackSize;
}
return count;
}
/** Check if inventory has at least one of a specific itemId. */
bool hasItem(const Ogre::String &itemId) const
{
return findItem(itemId) >= 0;
}
/** Check if inventory has at least one of a specific itemName. */
bool hasItemByName(const Ogre::String &itemName) const
{
return findItemByName(itemName) >= 0;
}
/** Recalculate total weight. */
void recalculateWeight()
{
totalWeight = 0.0f;
for (const auto &slot : slots) {
if (!slot.isEmpty())
totalWeight += slot.weight * slot.stackSize;
}
}
};
#endif // EDITSCENE_INVENTORY_HPP

View File

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

View File

@@ -0,0 +1,59 @@
#ifndef EDITSCENE_ITEM_HPP
#define EDITSCENE_ITEM_HPP
#pragma once
#include <Ogre.h>
#include <string>
#include <vector>
/**
* Item definition component.
*
* Attached to a world entity that represents a pickable item.
* The ActuatorSystem detects items (entities with ItemComponent)
* and shows "E - Pick up [ItemName]" prompts to the player.
*
* Items can also be placed in containers (chests, etc.) which
* have an InventoryComponent.
*
* For AI characters, behavior tree nodes (hasItem, pickupItem,
* dropItem, useItem, addItemToInventory) provide inventory access.
*/
struct ItemComponent {
// Display name of the item (e.g. "Apple", "Sword", "Key")
Ogre::String itemName = "Item";
// Item type for categorization (e.g. "food", "weapon", "key", "quest")
Ogre::String itemType = "misc";
// Unique identifier for this item definition
// Multiple entities can share the same itemId (e.g. multiple coins)
Ogre::String itemId;
// Stack size: how many of this item are in this stack
int stackSize = 1;
// Maximum stack size (0 = no stacking)
int maxStackSize = 99;
// Weight per unit (for encumbrance calculations)
float weight = 0.1f;
// Value (for trading)
int value = 1;
// Name of the GOAP action to execute when "using" this item
// (e.g. "eat", "equip", "read"). Empty = no use action.
Ogre::String useActionName;
ItemComponent() = default;
explicit ItemComponent(const Ogre::String &name,
const Ogre::String &type = "misc")
: itemName(name)
, itemType(type)
{
}
};
#endif // EDITSCENE_ITEM_HPP

View File

@@ -0,0 +1,19 @@
#include "Item.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/ItemEditor.hpp"
REGISTER_COMPONENT_GROUP("Item", "Game", ItemComponent, ItemEditor)
{
registry.registerComponent<ItemComponent>(
"Item", "Game", std::make_unique<ItemEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<ItemComponent>())
e.set<ItemComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<ItemComponent>())
e.remove<ItemComponent>();
});
}

View File

@@ -0,0 +1,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

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

View 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

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

View 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

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
#ifndef EDITSCENE_PATH_FOLLOWING_HPP
#define EDITSCENE_PATH_FOLLOWING_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <string>
#include <utility>
/**
* Animation state configuration for path following.
*
* Each state (e.g. "idle", "walk", "run") consists of unlimited
* state-machine-name -> state-name pairs applied via AnimationTreeSystem.
*/
struct PathFollowingState {
Ogre::String name;
std::vector<std::pair<Ogre::String, Ogre::String> > stateMachineStates;
PathFollowingState() = default;
explicit PathFollowingState(const Ogre::String &name_)
: name(name_)
{
}
};
/**
* Path Following component.
*
* Configures animation states for locomotion (idle/walk/run)
* and stores the current locomotion state. Used by GoapRunner
* to animate characters during plan execution.
*/
struct PathFollowingComponent {
// Animation state configurations
std::vector<PathFollowingState> pathFollowingStates = {
PathFollowingState("idle"),
PathFollowingState("walk"),
PathFollowingState("run"),
};
// Current locomotion state name
Ogre::String currentLocomotionState = "idle";
// Walk speed (m/s) for root motion scaling
float walkSpeed = 2.5f;
// Run speed (m/s) for root motion scaling
float runSpeed = 5.0f;
// Whether to use root motion
bool useRootMotion = true;
// Target position for path following (set by GoapRunner)
Ogre::Vector3 targetPosition = Ogre::Vector3::ZERO;
// Whether we have an active target
bool hasTarget = false;
// Path waypoints (set by GoapRunner, followed by PathFollowingSystem)
std::vector<Ogre::Vector3> path;
int pathIndex = 0;
float pathRecalcTimer = 0.0f;
PathFollowingComponent() = default;
const PathFollowingState *findState(const Ogre::String &name) const
{
for (const auto &state : pathFollowingStates) {
if (state.name == name)
return &state;
}
return nullptr;
}
};
#endif // EDITSCENE_PATH_FOLLOWING_HPP

View File

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

View File

@@ -0,0 +1,42 @@
#ifndef EDITSCENE_PLAYERCONTROLLER_HPP
#define EDITSCENE_PLAYERCONTROLLER_HPP
#pragma once
#include <Ogre.h>
/**
* Player controller component.
* Only active in game mode. Editable in editor mode.
*/
struct PlayerControllerComponent {
enum CameraMode { TPS = 0, FPS = 1 };
int cameraMode = TPS;
Ogre::String targetCharacterName = "";
Ogre::String fpsBoneName = "Head";
float tpsDistance = 3.0f;
float tpsHeight = 2.0f;
float mouseSensitivity = 0.2f;
/* Animation state machine configuration */
Ogre::String locomotionStateMachine = "locomotion";
Ogre::String idleState = "idle";
Ogre::String walkState = "walking";
Ogre::String runState = "running";
/* Swim animation states (used when character is in water) */
Ogre::String swimIdleState = "swim-idle";
Ogre::String swimState = "swim";
Ogre::String swimFastState = "swim-fast";
/* Actuator interaction settings */
float actuatorDistance = 25.0f;
float actuatorCooldown = 1.5f;
Ogre::Vector3 actuatorColor = Ogre::Vector3(0.0f, 0.4f, 1.0f);
float distantCircleRadius = 8.0f;
float nearCircleRadius = 14.0f;
float actuatorLabelFontSize = 14.0f;
/* Runtime: set by ActuatorSystem while executing an action */
bool inputLocked = false;
};
#endif // EDITSCENE_PLAYERCONTROLLER_HPP

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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;
}
};

View 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++;
}
};

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,409 @@
#include "Cursor3D.hpp"
#include "../components/Transform.hpp"
#include <cmath>
static const float COLOR_RED[3] = { 1.0f, 0.2f, 0.2f };
static const float COLOR_GREEN[3] = { 0.2f, 1.0f, 0.2f };
static const float COLOR_BLUE[3] = { 0.2f, 0.4f, 1.0f };
static const float COLOR_CYAN[3] = { 0.0f, 1.0f, 1.0f };
static const float COLOR_YELLOW[3] = { 1.0f, 1.0f, 0.0f };
static bool projectRayOntoAxis(const Ogre::Ray &ray,
const Ogre::Vector3 &axisOrigin,
const Ogre::Vector3 &axisDir, float &outT)
{
Ogre::Vector3 rayOrigin = ray.getOrigin();
Ogre::Vector3 rayDir = ray.getDirection();
Ogre::Vector3 w0 = rayOrigin - axisOrigin;
float a = rayDir.dotProduct(rayDir);
float b = rayDir.dotProduct(axisDir);
float c = axisDir.dotProduct(axisDir);
float d = rayDir.dotProduct(w0);
float e = axisDir.dotProduct(w0);
float denom = a * c - b * b;
if (std::abs(denom) < 0.0001f)
return false;
outT = (a * e - b * d) / denom;
return true;
}
Cursor3D::Cursor3D(Ogre::SceneManager *sceneMgr)
: m_sceneMgr(sceneMgr)
, m_cursorNode(nullptr)
, m_axisX(nullptr)
, m_axisY(nullptr)
, m_axisZ(nullptr)
, m_centerMarker(nullptr)
, m_position(Ogre::Vector3::ZERO)
, m_orientation(Ogre::Quaternion::IDENTITY)
, m_size(1.0f)
, m_visible(false)
, m_mode(Mode::Translate)
, m_selectedAxis(Axis::None)
, m_hoveredAxis(Axis::None)
, m_isDragging(false)
, m_dragStartT(0.0f)
, m_dragStartAngle(0.0f)
{
m_cursorNode = m_sceneMgr->getRootSceneNode()->createChildSceneNode(
"Cursor3DNode");
m_axisX = m_sceneMgr->createManualObject("CursorAxisX");
m_axisY = m_sceneMgr->createManualObject("CursorAxisY");
m_axisZ = m_sceneMgr->createManualObject("CursorAxisZ");
m_centerMarker = m_sceneMgr->createManualObject("CursorCenter");
m_cursorNode->attachObject(m_axisX);
m_cursorNode->attachObject(m_axisY);
m_cursorNode->attachObject(m_axisZ);
m_cursorNode->attachObject(m_centerMarker);
m_axisX->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
m_axisY->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
m_axisZ->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
m_centerMarker->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
m_cursorNode->setVisible(false);
createGeometry();
}
void Cursor3D::shutdown()
{
if (!m_sceneMgr)
return;
auto detach = [&](Ogre::ManualObject *obj) {
if (m_cursorNode && obj) {
try {
m_cursorNode->detachObject(obj);
} catch (...) {
}
}
};
auto destroy = [&](Ogre::ManualObject *obj) {
if (obj) {
try {
m_sceneMgr->destroyManualObject(obj);
} catch (...) {
}
}
};
detach(m_axisX);
detach(m_axisY);
detach(m_axisZ);
detach(m_centerMarker);
destroy(m_axisX);
m_axisX = nullptr;
destroy(m_axisY);
m_axisY = nullptr;
destroy(m_axisZ);
m_axisZ = nullptr;
destroy(m_centerMarker);
m_centerMarker = nullptr;
if (m_cursorNode) {
try {
m_sceneMgr->destroySceneNode(m_cursorNode);
} catch (...) {
}
m_cursorNode = nullptr;
}
m_sceneMgr = nullptr;
}
Cursor3D::~Cursor3D()
{
if (m_sceneMgr)
shutdown();
}
void Cursor3D::setPosition(const Ogre::Vector3 &pos)
{
m_position = pos;
updateNodeTransform();
}
void Cursor3D::setOrientation(const Ogre::Quaternion &rot)
{
m_orientation = rot;
updateNodeTransform();
}
void Cursor3D::setVisible(bool visible)
{
m_visible = visible;
if (m_cursorNode)
m_cursorNode->setVisible(visible);
}
bool Cursor3D::isVisible() const
{
return m_visible;
}
void Cursor3D::setSize(float size)
{
m_size = size;
createGeometry();
}
void Cursor3D::snapToTransform(const TransformComponent &transform)
{
if (transform.node) {
m_position = transform.node->_getDerivedPosition();
m_orientation = transform.node->_getDerivedOrientation();
} else {
m_position = transform.position;
m_orientation = transform.rotation;
}
updateNodeTransform();
}
void Cursor3D::applyToTransform(TransformComponent &transform) const
{
if (!transform.node)
return;
Ogre::SceneNode *parent = static_cast<Ogre::SceneNode *>(
transform.node->getParent());
if (parent) {
transform.position = parent->convertWorldToLocalPosition(
m_position);
transform.rotation = parent->convertWorldToLocalOrientation(
m_orientation);
} else {
transform.position = m_position;
transform.rotation = m_orientation;
}
transform.applyToNode();
transform.markChanged();
}
void Cursor3D::updateNodeTransform()
{
if (m_cursorNode) {
m_cursorNode->setPosition(m_position);
m_cursorNode->setOrientation(m_orientation);
}
}
void Cursor3D::createGeometry()
{
float len = 0.5f * m_size;
float half = 0.05f * m_size;
// X Axis
bool xSel = (m_selectedAxis == Axis::X || m_hoveredAxis == Axis::X);
m_axisX->clear();
m_axisX->begin("Ogre/AxisGizmo",
Ogre::RenderOperation::OT_LINE_LIST);
m_axisX->colour(xSel ? COLOR_YELLOW[0] : COLOR_RED[0],
xSel ? COLOR_YELLOW[1] : COLOR_RED[1],
xSel ? COLOR_YELLOW[2] : COLOR_RED[2]);
m_axisX->position(0, 0, 0);
m_axisX->position(len, 0, 0);
m_axisX->end();
// Y Axis
bool ySel = (m_selectedAxis == Axis::Y || m_hoveredAxis == Axis::Y);
m_axisY->clear();
m_axisY->begin("Ogre/AxisGizmo",
Ogre::RenderOperation::OT_LINE_LIST);
m_axisY->colour(ySel ? COLOR_YELLOW[0] : COLOR_GREEN[0],
ySel ? COLOR_YELLOW[1] : COLOR_GREEN[1],
ySel ? COLOR_YELLOW[2] : COLOR_GREEN[2]);
m_axisY->position(0, 0, 0);
m_axisY->position(0, len, 0);
m_axisY->end();
// Z Axis
bool zSel = (m_selectedAxis == Axis::Z || m_hoveredAxis == Axis::Z);
m_axisZ->clear();
m_axisZ->begin("Ogre/AxisGizmo",
Ogre::RenderOperation::OT_LINE_LIST);
m_axisZ->colour(zSel ? COLOR_YELLOW[0] : COLOR_BLUE[0],
zSel ? COLOR_YELLOW[1] : COLOR_BLUE[1],
zSel ? COLOR_YELLOW[2] : COLOR_BLUE[2]);
m_axisZ->position(0, 0, 0);
m_axisZ->position(0, 0, len);
m_axisZ->end();
// Center marker
m_centerMarker->clear();
m_centerMarker->begin("Ogre/AxisGizmo",
Ogre::RenderOperation::OT_LINE_LIST);
m_centerMarker->colour(COLOR_CYAN[0], COLOR_CYAN[1],
COLOR_CYAN[2]);
Ogre::Vector3 c[8] = {
Ogre::Vector3(-half, -half, -half),
Ogre::Vector3(half, -half, -half),
Ogre::Vector3(half, half, -half),
Ogre::Vector3(-half, half, -half),
Ogre::Vector3(-half, -half, half),
Ogre::Vector3(half, -half, half),
Ogre::Vector3(half, half, half),
Ogre::Vector3(-half, half, half),
};
auto line = [&](int a, int b) {
m_centerMarker->position(c[a]);
m_centerMarker->position(c[b]);
};
line(0, 1);
line(1, 2);
line(2, 3);
line(3, 0);
line(4, 5);
line(5, 6);
line(6, 7);
line(7, 4);
line(0, 4);
line(1, 5);
line(2, 6);
line(3, 7);
m_centerMarker->end();
}
Cursor3D::Axis Cursor3D::hitTest(const Ogre::Ray &mouseRay)
{
if (!m_cursorNode || !m_axisX->isVisible())
return Axis::None;
Ogre::Vector3 cursorPos = m_cursorNode->getPosition();
float len = 0.5f * m_size;
float threshold = 0.15f * m_size;
float bestDist = 1000000.0f;
Axis bestAxis = Axis::None;
for (int i = 0; i < 3; ++i) {
Ogre::Vector3 axisDir;
if (i == 0)
axisDir = m_cursorNode->getOrientation() *
Ogre::Vector3::UNIT_X;
else if (i == 1)
axisDir = m_cursorNode->getOrientation() *
Ogre::Vector3::UNIT_Y;
else
axisDir = m_cursorNode->getOrientation() *
Ogre::Vector3::UNIT_Z;
for (int j = 0; j <= 8; ++j) {
float t = len * j / 8.0f;
Ogre::Vector3 pointOnAxis = cursorPos + axisDir * t;
Ogre::Vector3 L = pointOnAxis - mouseRay.getOrigin();
float tca = L.dotProduct(mouseRay.getDirection());
if (tca < 0.01f)
continue;
float d2 = L.dotProduct(L) - tca * tca;
if (d2 <= threshold * threshold) {
if (tca < bestDist) {
bestDist = tca;
bestAxis = static_cast<Axis>(i + 1);
}
}
}
}
return bestAxis;
}
bool Cursor3D::onMousePressed(const Ogre::Ray &mouseRay,
const Ogre::Camera *camera)
{
(void)camera;
if (!m_axisX->isVisible())
return false;
m_selectedAxis = hitTest(mouseRay);
if (m_selectedAxis != Axis::None) {
m_isDragging = true;
m_dragStartPosition = m_position;
m_dragStartRotation = m_orientation;
Ogre::Vector3 axisDir;
switch (m_selectedAxis) {
case Axis::X:
axisDir = m_orientation * Ogre::Vector3::UNIT_X;
break;
case Axis::Y:
axisDir = m_orientation * Ogre::Vector3::UNIT_Y;
break;
case Axis::Z:
axisDir = m_orientation * Ogre::Vector3::UNIT_Z;
break;
default:
axisDir = Ogre::Vector3::UNIT_X;
break;
}
m_dragAxisDir = axisDir;
if (m_mode == Mode::Translate) {
projectRayOntoAxis(mouseRay, m_dragStartPosition,
m_dragAxisDir, m_dragStartT);
} else if (m_mode == Mode::Rotate) {
(void)camera;
m_dragStartAngle = 0.0f;
}
createGeometry();
return true;
}
return false;
}
bool Cursor3D::onMouseMoved(const Ogre::Ray &mouseRay,
const Ogre::Vector2 &mouseDelta)
{
if (m_isDragging && m_selectedAxis != Axis::None) {
if (m_mode == Mode::Translate) {
float currentT;
if (!projectRayOntoAxis(mouseRay, m_dragStartPosition,
m_dragAxisDir, currentT)) {
return true;
}
float deltaT = currentT - m_dragStartT;
Ogre::Vector3 newPos =
m_dragStartPosition + m_dragAxisDir * deltaT;
setPosition(newPos);
return true;
} else if (m_mode == Mode::Rotate) {
// Simple axis-based rotation from mouse delta
float deltaAngle = 0.0f;
if (m_selectedAxis == Axis::X) {
// Up/down rotates around X
deltaAngle = mouseDelta.y * 0.5f;
} else if (m_selectedAxis == Axis::Y) {
// Left/right rotates around Y
deltaAngle = -mouseDelta.x * 0.5f;
} else if (m_selectedAxis == Axis::Z) {
// Left/right rotates around Z
deltaAngle = -mouseDelta.x * 0.5f;
}
m_dragStartAngle += deltaAngle;
Ogre::Quaternion rot(
Ogre::Degree(m_dragStartAngle),
m_dragAxisDir);
Ogre::Quaternion newRot = rot * m_dragStartRotation;
setOrientation(newRot);
return true;
}
} else if (m_axisX->isVisible()) {
Axis prevHover = m_hoveredAxis;
m_hoveredAxis = hitTest(mouseRay);
if (prevHover != m_hoveredAxis)
createGeometry();
}
return false;
}
bool Cursor3D::onMouseReleased()
{
if (m_isDragging) {
m_isDragging = false;
m_selectedAxis = Axis::None;
m_hoveredAxis = Axis::None;
createGeometry();
return true;
}
return false;
}

View File

@@ -0,0 +1,96 @@
#ifndef EDITSCENE_CURSOR3D_HPP
#define EDITSCENE_CURSOR3D_HPP
#pragma once
#include <Ogre.h>
#include <OgreManualObject.h>
// Forward declarations
struct TransformComponent;
/**
* 3D Cursor - a visual marker for prefab placement and transform reference.
* Supports axis-based translation and rotation interaction like the gizmo.
*/
class Cursor3D {
public:
enum class Mode {
Translate,
Rotate
};
enum class Axis {
None,
X,
Y,
Z
};
Cursor3D(Ogre::SceneManager *sceneMgr);
~Cursor3D();
void shutdown();
void setPosition(const Ogre::Vector3 &pos);
Ogre::Vector3 getPosition() const { return m_position; }
void setOrientation(const Ogre::Quaternion &rot);
Ogre::Quaternion getOrientation() const { return m_orientation; }
void setVisible(bool visible);
bool isVisible() const;
void setSize(float size);
float getSize() const { return m_size; }
void setMode(Mode mode) { m_mode = mode; }
Mode getMode() const { return m_mode; }
void snapToTransform(const TransformComponent &transform);
void applyToTransform(TransformComponent &transform) const;
/**
* Handle mouse input for cursor interaction.
* Returns true if cursor handled the input.
*/
bool onMousePressed(const Ogre::Ray &mouseRay,
const Ogre::Camera *camera);
bool onMouseMoved(const Ogre::Ray &mouseRay,
const Ogre::Vector2 &mouseDelta);
bool onMouseReleased();
bool isDragging() const { return m_isDragging; }
Axis getSelectedAxis() const { return m_selectedAxis; }
private:
void createGeometry();
void updateNodeTransform();
Axis hitTest(const Ogre::Ray &mouseRay);
Ogre::SceneManager *m_sceneMgr;
Ogre::SceneNode *m_cursorNode;
Ogre::ManualObject *m_axisX;
Ogre::ManualObject *m_axisY;
Ogre::ManualObject *m_axisZ;
Ogre::ManualObject *m_centerMarker;
Ogre::Vector3 m_position;
Ogre::Quaternion m_orientation;
float m_size;
bool m_visible;
Mode m_mode;
Axis m_selectedAxis;
Axis m_hoveredAxis;
bool m_isDragging;
// Drag state
Ogre::Vector3 m_dragStartPosition;
Ogre::Quaternion m_dragStartRotation;
Ogre::Vector3 m_dragAxisDir;
float m_dragStartT;
float m_dragStartAngle;
Ogre::Vector2 m_dragScreenAxis;
};
#endif // EDITSCENE_CURSOR3D_HPP

View File

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

View File

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

View File

@@ -0,0 +1,418 @@
-- =============================================================================
-- Action Database Lua API Examples
-- =============================================================================
-- This file demonstrates how to create, query, and manage GOAP actions and
-- goals from Lua using the ecs.action_db API.
--
-- The ActionDatabase is a global singleton. Actions and goals defined here
-- are immediately available to all characters in the scene.
-- =============================================================================
-- =============================================================================
-- Defining Bit Names
-- =============================================================================
-- Before using bits in preconditions/effects, you should define meaningful
-- names for the 64 available bit slots. This makes your actions readable.
--
-- Bit names are global across the entire game session. They map
-- human-readable names (like "has_axe", "is_hungry") to bit indices
-- (0-63) used in GoapBlackboard preconditions and effects.
--
-- You can define bits explicitly at startup:
-- =============================================================================
-- Explicitly assign bit names to specific indices:
ecs.action_db.set_bit_name(0, "has_axe")
ecs.action_db.set_bit_name(1, "has_wood")
ecs.action_db.set_bit_name(2, "is_hungry")
ecs.action_db.set_bit_name(3, "near_tree")
ecs.action_db.set_bit_name(4, "near_well")
ecs.action_db.set_bit_name(5, "has_bucket")
ecs.action_db.set_bit_name(6, "has_food")
ecs.action_db.set_bit_name(7, "near_fire")
ecs.action_db.set_bit_name(8, "has_cooked_food")
ecs.action_db.set_bit_name(9, "is_awake")
ecs.action_db.set_bit_name(10, "at_market")
ecs.action_db.set_bit_name(11, "at_home")
ecs.action_db.set_bit_name(12, "near_chair")
ecs.action_db.set_bit_name(13, "is_sitting")
ecs.action_db.set_bit_name(14, "near_forest")
ecs.action_db.set_bit_name(15, "is_strong")
ecs.action_db.set_bit_name(16, "has_strength")
-- Or use auto_assign_bit() to let the system pick the index:
local idx = ecs.action_db.auto_assign_bit("has_water")
print("'has_water' assigned to bit " .. idx)
-- Look up a bit by name:
local bit_idx = ecs.action_db.find_bit_by_name("has_axe")
print("'has_axe' is at bit " .. bit_idx)
-- Get the name for a bit index:
local name = ecs.action_db.get_bit_name(0)
print("Bit 0 is named '" .. name .. "'")
-- List all currently assigned bit names:
local bits = ecs.action_db.list_bit_names()
print("Assigned bit names:")
for _, b in ipairs(bits) do
print(" Bit " .. b.index .. ": " .. b.name)
end
-- NOTE: If you use a bit name in an action's preconditions/effects
-- that hasn't been explicitly assigned, it will be auto-assigned
-- to the first free slot automatically. So you don't HAVE to
-- pre-define them, but it's good practice for clarity.
-- =============================================================================
-- Creating Actions
-- =============================================================================
-- Simple action with just a name and cost:
ecs.action_db.add_action("idle", 1)
-- Action with preconditions (what must be true before the action can run):
ecs.action_db.add_action("chop_wood", 2,
{
bits = { has_axe = true },
values = { stamina = 10 }
},
{ -- effects (what becomes true after the action runs)
bits = { has_wood = true },
values = { stamina = -5 }
}
)
-- Action with only preconditions, no effects:
ecs.action_db.add_action("fetch_water", 3,
{
bits = { near_well = true, has_bucket = true },
values = { thirst = 50 }
}
)
-- Action with only effects, no preconditions:
ecs.action_db.add_action("rest", 1,
{},
{
values = { stamina = 100, energy = 100 }
}
)
-- Action with float and string values in blackboard:
ecs.action_db.add_action("cook_food", 4,
{
bits = { has_food = true, near_fire = true },
values = { cooking_skill = 3 },
floatValues = { hunger = 50.0 }
},
{
bits = { has_cooked_food = true },
values = { hunger = -30 },
stringValues = { last_action = "cooking" }
}
)
-- =============================================================================
-- Creating Actions WITH Behavior Trees
-- =============================================================================
-- Action with a simple leaf behavior tree (plays an animation):
ecs.action_db.add_action("wave", 1,
{}, -- no preconditions
{}, -- no effects
{ -- behavior tree (arg 5)
type = "sequence",
children = {
{ type = "setAnimationState", name = "SM/Wave" },
{ type = "delay", params = "2.0" },
{ type = "setAnimationState", name = "SM/Idle" }
}
}
)
-- Action with a selector behavior tree (try to chop, fall back to idle):
ecs.action_db.add_action("chop_tree", 3,
{
bits = { near_tree = true },
values = { stamina = 15 }
},
{
bits = { has_wood = true },
values = { stamina = -8, wood_count = 1 }
},
{
type = "selector",
children = {
{
type = "sequence",
children = {
{ type = "checkBit", name = "has_axe", params = "1" },
{ type = "setAnimationState", name = "SM/Chop" },
{ type = "delay", params = "3.0" },
{ type = "setAnimationState", name = "SM/Idle" },
{ type = "setBit", name = "has_wood", params = "1" }
}
},
{
type = "sequence",
children = {
{ type = "debugPrint", name = "No axe! Picking up stick..." },
{ type = "setAnimationState", name = "SM/Pickup" },
{ type = "delay", params = "1.0" },
{ type = "setBit", name = "has_wood", params = "1" }
}
}
}
}
)
-- Action with blackboard checks and value manipulation:
ecs.action_db.add_action("travel_to_market", 5,
{
bits = { is_awake = true },
values = { energy = 20 }
},
{
bits = { at_market = true },
values = { energy = -15 }
},
{
type = "sequence",
children = {
{ type = "checkValue", name = "energy", params = ">= 20" },
{ type = "setAnimationState", name = "SM/Walk" },
{ type = "delay", params = "5.0" },
{ type = "setAnimationState", name = "SM/Idle" },
{ type = "setBit", name = "at_market", params = "1" },
{ type = "setBit", name = "at_home", params = "0" }
}
}
)
-- Action with inventory operations:
ecs.action_db.add_action("gather_wood", 2,
{
bits = { near_forest = true }
},
{
values = { wood_count = 3 }
},
{
type = "sequence",
children = {
{ type = "setAnimationState", name = "SM/Gather" },
{ type = "delay", params = "2.0" },
{ type = "addItemToInventory", params = "wood,Firewood,material,3,1.0,0" },
{ type = "setAnimationState", name = "SM/Idle" }
}
}
)
-- Action that teleports character to a smart object child:
ecs.action_db.add_action("sit_on_chair", 1,
{
bits = { near_chair = true }
},
{
bits = { is_sitting = true }
},
{
type = "sequence",
children = {
{ type = "teleportToChild", name = "SitTarget" },
{ type = "disablePhysics" },
{ type = "setAnimationState", name = "SM/Sit" },
{ type = "delay", params = "5.0" },
{ type = "setAnimationState", name = "SM/Stand" },
{ type = "enablePhysics" }
}
}
)
-- =============================================================================
-- Creating Goals
-- =============================================================================
-- Simple goal with just a name and priority:
ecs.action_db.add_goal("survive", 100)
-- Goal with a target blackboard state:
ecs.action_db.add_goal("gather_resources", 50,
{
bits = { has_wood = true, has_water = true },
values = { wood_count = 5, water_count = 3 }
}
)
-- Goal with a condition expression (evaluated against character's blackboard):
ecs.action_db.add_goal("stay_healthy", 80,
{
values = { health = 100, stamina = 80 }
},
"health < 50 || stamina < 30" -- only valid when character needs healing
)
-- Goal with full specification:
ecs.action_db.add_goal("become_strong", 30,
{
bits = { is_strong = true },
values = { strength = 100 }
},
"strength < 100" -- only valid if not already strong
)
-- =============================================================================
-- Querying Actions and Goals
-- =============================================================================
-- Find an action by name:
local action = ecs.action_db.find_action("chop_wood")
if action then
print("Found action: " .. action.name .. " (cost: " .. action.cost .. ")")
-- action.preconditions and action.effects are tables with:
-- .bits - table of boolean flags
-- .values - table of integer values
-- .floatValues - table of float values
-- .stringValues - table of string values
-- action.behaviorTree is a table with:
-- .type - node type string
-- .name - optional name
-- .params - optional params
-- .children - optional array of child nodes
end
-- Find a goal by name:
local goal = ecs.action_db.find_goal("gather_resources")
if goal then
print("Found goal: " .. goal.name .. " (priority: " .. goal.priority .. ")")
-- goal.target is a blackboard table
-- goal.condition is the condition string
end
-- =============================================================================
-- Listing All Actions and Goals
-- =============================================================================
-- List all action names:
local actions = ecs.action_db.list_actions()
print("Available actions:")
for i, name in ipairs(actions) do
print(" " .. i .. ". " .. name)
end
-- List all goal names:
local goals = ecs.action_db.list_goals()
print("Available goals:")
for i, name in ipairs(goals) do
print(" " .. i .. ". " .. name)
end
-- =============================================================================
-- Removing Actions and Goals
-- =============================================================================
-- Remove an action by name:
local removed = ecs.action_db.remove_action("idle")
if removed then
print("Removed action: idle")
end
-- Remove a goal by name:
ecs.action_db.remove_goal("become_strong")
-- =============================================================================
-- Replacing Actions (same name = replace)
-- =============================================================================
-- If you add an action with the same name as an existing one, it replaces it:
ecs.action_db.add_action("chop_wood", 5,
{
bits = { has_axe = true, has_strength = true },
values = { stamina = 20 }
},
{
bits = { has_wood = true },
values = { stamina = -10, wood_count = 2 }
}
)
-- The old "chop_wood" action is replaced with this new definition.
-- =============================================================================
-- Clearing Everything
-- =============================================================================
-- Remove all actions and goals:
-- ecs.action_db.clear()
-- =============================================================================
-- Blackboard Table Format Reference
-- =============================================================================
--
-- The blackboard table passed to add_action/add_goal has this structure:
--
-- {
-- bits = {
-- has_axe = true, -- boolean flags (use named bits)
-- has_wood = false,
-- is_hungry = true
-- },
-- values = { -- integer values
-- health = 100,
-- stamina = 50,
-- wood_count = 0
-- },
-- floatValues = { -- float values
-- hunger = 75.5,
-- speed = 1.2
-- },
-- stringValues = { -- string values
-- last_action = "idle",
-- current_state = "exploring"
-- }
-- }
--
-- All sub-tables are optional. An empty table or nil means no constraints.
-- =============================================================================
-- =============================================================================
-- Behavior Tree Table Format Reference
-- =============================================================================
--
-- The behaviorTree table (arg 5 of add_action) has this structure:
--
-- {
-- type = "sequence", -- node type (required)
-- name = "optional_name", -- depends on type (task name, anim state, etc.)
-- params = "optional_params", -- extra parameters (delay seconds, bit index, etc.)
-- children = { -- array of child nodes (for sequence/selector/invert)
-- { type = "task", name = "myAction" },
-- { type = "setAnimationState", name = "SM/Walk" },
-- { type = "delay", params = "2.0" },
-- { type = "checkBit", name = "has_axe", params = "1" }
-- }
-- }
--
-- Common node types:
-- "sequence" - Execute children in order until one fails
-- "selector" - Execute children in order until one succeeds
-- "invert" - Invert the result of a single child
-- "task" - Leaf: references a named task
-- "check" - Leaf: references a named condition
-- "debugPrint" - Leaf: prints 'name' to console
-- "setAnimationState"- Leaf: sets animation state (name="SM/State")
-- "isAnimationEnded" - Leaf: true if animation ended
-- "setBit" - Leaf: sets blackboard bit (name=bit, params=0/1)
-- "checkBit" - Leaf: true if blackboard bit is set
-- "setValue" - Leaf: sets blackboard value (name=key, params=val)
-- "checkValue" - Leaf: blackboard comparison (name=key, params="op val")
-- "delay" - Leaf: waits N seconds (params=seconds as float)
-- "teleportToChild" - Leaf: teleports to named child of Smart Object
-- "disablePhysics" - Leaf: removes character from physics
-- "enablePhysics" - Leaf: re-adds character to physics
-- "hasItem" - Leaf check: true if inventory has itemId
-- "pickupItem" - Leaf: picks up nearest item
-- "dropItem" - Leaf: drops item from inventory
-- "useItem" - Leaf: uses item from inventory
-- "addItemToInventory"- Leaf: adds item directly to inventory
-- =============================================================================

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