Compare commits
2 Commits
cca732b41b
...
02fa78764a
| Author | SHA1 | Date | |
|---|---|---|---|
| 02fa78764a | |||
| abe6eef6b3 |
@@ -65,6 +65,12 @@ set(EDITSCENE_SOURCES
|
||||
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
|
||||
@@ -129,6 +135,9 @@ set(EDITSCENE_SOURCES
|
||||
components/CellGrid.cpp
|
||||
components/StartupMenuModule.cpp
|
||||
components/PlayerControllerModule.cpp
|
||||
components/DialogueComponentModule.cpp
|
||||
systems/DialogueSystem.cpp
|
||||
ui/DialogueEditor.cpp
|
||||
components/BuoyancyInfoModule.cpp
|
||||
components/WaterPhysicsModule.cpp
|
||||
components/WaterPlaneModule.cpp
|
||||
@@ -138,6 +147,10 @@ set(EDITSCENE_SOURCES
|
||||
gizmo/Gizmo.cpp
|
||||
gizmo/Cursor3D.cpp
|
||||
physics/physics.cpp
|
||||
lua/LuaState.cpp
|
||||
lua/LuaEntityApi.cpp
|
||||
lua/LuaComponentApi.cpp
|
||||
lua/LuaEventApi.cpp
|
||||
)
|
||||
|
||||
set(EDITSCENE_HEADERS
|
||||
@@ -169,6 +182,9 @@ set(EDITSCENE_HEADERS
|
||||
components/CellGrid.hpp
|
||||
components/StartupMenu.hpp
|
||||
components/PlayerController.hpp
|
||||
components/DialogueComponent.hpp
|
||||
systems/DialogueSystem.hpp
|
||||
ui/DialogueEditor.hpp
|
||||
systems/StartupMenuSystem.hpp
|
||||
systems/PlayerControllerSystem.hpp
|
||||
systems/EditorUISystem.hpp
|
||||
@@ -208,6 +224,12 @@ set(EDITSCENE_HEADERS
|
||||
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
|
||||
@@ -273,6 +295,10 @@ set(EDITSCENE_HEADERS
|
||||
gizmo/Gizmo.hpp
|
||||
gizmo/Cursor3D.hpp
|
||||
physics/physics.h
|
||||
lua/LuaState.hpp
|
||||
lua/LuaEntityApi.hpp
|
||||
lua/LuaComponentApi.hpp
|
||||
lua/LuaEventApi.hpp
|
||||
)
|
||||
|
||||
add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS})
|
||||
@@ -294,6 +320,7 @@ target_link_libraries(editSceneEditor
|
||||
RecastNavigation::DetourTileCache
|
||||
RecastNavigation::DetourCrowd
|
||||
RecastNavigation::DebugUtils
|
||||
lua
|
||||
)
|
||||
|
||||
target_include_directories(editSceneEditor PRIVATE
|
||||
@@ -303,6 +330,8 @@ target_include_directories(editSceneEditor PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DetourTileCache/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DetourCrowd/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DebugUtils/Include
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lpeg-1.1.0
|
||||
)
|
||||
|
||||
# Copy local resources (materials, etc.)
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "systems/NormalDebugSystem.hpp"
|
||||
#include "systems/RoomLayoutSystem.hpp"
|
||||
#include "systems/StartupMenuSystem.hpp"
|
||||
#include "systems/DialogueSystem.hpp"
|
||||
#include "systems/PlayerControllerSystem.hpp"
|
||||
#include "systems/SceneSerializer.hpp"
|
||||
#include "camera/EditorCamera.hpp"
|
||||
@@ -58,6 +59,7 @@
|
||||
#include "components/AnimationTreeTemplate.hpp"
|
||||
#include "components/Character.hpp"
|
||||
#include "components/StartupMenu.hpp"
|
||||
#include "components/DialogueComponent.hpp"
|
||||
#include "components/PlayerController.hpp"
|
||||
#include "components/CellGrid.hpp"
|
||||
#include "components/CellGridModule.hpp"
|
||||
@@ -75,10 +77,16 @@
|
||||
#include "components/PathFollowing.hpp"
|
||||
#include "systems/ActuatorSystem.hpp"
|
||||
#include "systems/EventHandlerSystem.hpp"
|
||||
#include "systems/ItemSystem.hpp"
|
||||
#include "components/EventHandler.hpp"
|
||||
#include "components/Item.hpp"
|
||||
#include "components/Inventory.hpp"
|
||||
|
||||
#include <OgreRTShaderSystem.h>
|
||||
#include <imgui.h>
|
||||
#include "lua/LuaEntityApi.hpp"
|
||||
#include "lua/LuaComponentApi.hpp"
|
||||
#include "lua/LuaEventApi.hpp"
|
||||
|
||||
//=============================================================================
|
||||
// ImGuiRenderListener Implementation
|
||||
@@ -130,6 +138,16 @@ void ImGuiRenderListener::preViewportUpdate(
|
||||
if (sms)
|
||||
sms->update(m_deltaTime);
|
||||
}
|
||||
|
||||
// Render dialogue box in game mode (inside ImGui frame scope)
|
||||
if (m_editorApp &&
|
||||
m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
|
||||
m_editorApp->getGamePlayState() ==
|
||||
EditorApp::GamePlayState::Playing) {
|
||||
DialogueSystem *ds = m_editorApp->getDialogueSystem();
|
||||
if (ds)
|
||||
ds->update(m_deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiRenderListener::postViewportUpdate(
|
||||
@@ -175,22 +193,27 @@ EditorApp::~EditorApp()
|
||||
// This ensures all components with Ogre resources are cleaned up while SceneManager exists
|
||||
// Collect entities first, then delete after iteration (can't modify during iteration)
|
||||
std::vector<flecs::entity> entitiesToDelete;
|
||||
m_world.query<EditorMarkerComponent>().each(
|
||||
[&](flecs::entity e, EditorMarkerComponent) {
|
||||
entitiesToDelete.push_back(e);
|
||||
});
|
||||
for (auto &e : entitiesToDelete) {
|
||||
if (e.is_alive()) {
|
||||
e.destruct();
|
||||
}
|
||||
}
|
||||
|
||||
// Release all systems
|
||||
m_playerControllerSystem.reset();
|
||||
// Destroy dialogue system before other systems
|
||||
m_dialogueSystem.reset();
|
||||
|
||||
m_startupMenuSystem.reset();
|
||||
m_characterSlotSystem.reset();
|
||||
m_animationTreeSystem.reset();
|
||||
m_playerControllerSystem.reset();
|
||||
m_itemSystem.reset();
|
||||
m_eventHandlerSystem.reset();
|
||||
m_actuatorSystem.reset();
|
||||
m_goapPlannerSystem.reset();
|
||||
m_pathFollowingSystem.reset();
|
||||
m_goapRunnerSystem.reset();
|
||||
m_smartObjectSystem.reset();
|
||||
m_roomLayoutSystem.reset();
|
||||
m_normalDebugSystem.reset();
|
||||
m_cellGridSystem.reset();
|
||||
m_characterSystem.reset();
|
||||
m_navMeshSystem.reset();
|
||||
m_behaviorTreeSystem.reset();
|
||||
m_animationTreeSystem.reset();
|
||||
m_characterSlotSystem.reset();
|
||||
m_proceduralMeshSystem.reset();
|
||||
m_proceduralMaterialSystem.reset();
|
||||
m_proceduralTextureSystem.reset();
|
||||
@@ -255,7 +278,7 @@ void EditorApp::setup()
|
||||
if (m_uiSystem)
|
||||
m_uiSystem->setEditorUIEnabled(m_gameMode ==
|
||||
GameMode::Editor);
|
||||
m_uiSystem->setEditorCamera(m_camera.get());
|
||||
m_uiSystem->setEditorCamera(m_camera.get());
|
||||
|
||||
// Setup physics system
|
||||
m_physicsSystem = std::make_unique<EditorPhysicsSystem>(
|
||||
@@ -362,6 +385,13 @@ void EditorApp::setup()
|
||||
m_eventHandlerSystem = std::make_unique<EventHandlerSystem>(
|
||||
m_world, m_behaviorTreeSystem.get());
|
||||
|
||||
// Setup Item system
|
||||
m_itemSystem = std::make_unique<ItemSystem>(
|
||||
m_world, m_sceneMgr, this, m_behaviorTreeSystem.get());
|
||||
|
||||
// Wire ItemSystem into SmartObjectSystem for BT access
|
||||
m_smartObjectSystem->setItemSystem(m_itemSystem.get());
|
||||
|
||||
// Setup GOAP Runner system
|
||||
m_goapRunnerSystem = std::make_unique<GoapRunnerSystem>(
|
||||
m_world, m_sceneMgr, m_smartObjectSystem.get(),
|
||||
@@ -371,8 +401,8 @@ void EditorApp::setup()
|
||||
m_animationTreeSystem.get());
|
||||
|
||||
// Setup GOAP Planner system
|
||||
m_goapPlannerSystem = std::make_unique<GoapPlannerSystem>(
|
||||
m_world);
|
||||
m_goapPlannerSystem =
|
||||
std::make_unique<GoapPlannerSystem>(m_world);
|
||||
m_goapPlannerSystem->setEditorApp(this);
|
||||
|
||||
// Setup Path Following system
|
||||
@@ -408,6 +438,8 @@ void EditorApp::setup()
|
||||
// Setup game systems
|
||||
m_startupMenuSystem = std::make_unique<StartupMenuSystem>(
|
||||
m_world, m_sceneMgr, this);
|
||||
m_dialogueSystem = std::make_unique<DialogueSystem>(
|
||||
m_world, m_sceneMgr, this);
|
||||
m_playerControllerSystem =
|
||||
std::make_unique<PlayerControllerSystem>(
|
||||
m_world, m_sceneMgr, this);
|
||||
@@ -430,10 +462,11 @@ void EditorApp::setup()
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-load menu font before showing overlay
|
||||
// (OGRE builds the atlas in createFontTexture() during show())
|
||||
// Pre-load fonts before showing overlay
|
||||
if (m_startupMenuSystem)
|
||||
m_startupMenuSystem->prepareFont();
|
||||
if (m_dialogueSystem)
|
||||
m_dialogueSystem->prepareFont();
|
||||
|
||||
// Now show the overlay — font atlas will be built with our font
|
||||
if (m_imguiOverlay)
|
||||
@@ -454,6 +487,28 @@ void EditorApp::setup()
|
||||
addInputListener(this);
|
||||
addInputListener(getImGuiInputListener());
|
||||
|
||||
// Initialize Lua scripting
|
||||
{
|
||||
lua_State *L = m_lua.getState();
|
||||
|
||||
// Store the Flecs world pointer in the Lua registry
|
||||
// so Lua API functions can access it.
|
||||
flecs::world *worldPtr = &m_world;
|
||||
lua_pushlightuserdata(L, worldPtr);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX,
|
||||
"EditSceneFlecsWorld");
|
||||
|
||||
// Register all Lua API modules.
|
||||
// Order matters: Entity API creates the "ecs" table,
|
||||
// Component and Event APIs add to it.
|
||||
editScene::registerLuaEntityApi(L);
|
||||
editScene::registerLuaComponentApi(L);
|
||||
editScene::registerLuaEventApi(L);
|
||||
|
||||
// Run late setup: load data.lua and initial scripts.
|
||||
m_lua.lateSetup();
|
||||
}
|
||||
|
||||
// Game mode can be set externally before setup() is called
|
||||
m_setupComplete = true;
|
||||
|
||||
@@ -605,6 +660,7 @@ void EditorApp::setupECS()
|
||||
|
||||
// Register game components
|
||||
m_world.component<StartupMenuComponent>();
|
||||
m_world.component<DialogueComponent>();
|
||||
m_world.component<PlayerControllerComponent>();
|
||||
m_world.component<InWater>();
|
||||
|
||||
@@ -645,6 +701,10 @@ void EditorApp::setupECS()
|
||||
|
||||
// Register PrefabInstance component
|
||||
m_world.component<PrefabInstanceComponent>();
|
||||
|
||||
// Register Item and Inventory components
|
||||
m_world.component<ItemComponent>();
|
||||
m_world.component<InventoryComponent>();
|
||||
}
|
||||
|
||||
void EditorApp::createDefaultEntities()
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <OgreRenderTargetListener.h>
|
||||
#include <flecs.h>
|
||||
#include <memory>
|
||||
#include "lua/LuaState.hpp"
|
||||
|
||||
// Forward declarations
|
||||
class EditorUISystem;
|
||||
@@ -29,6 +30,7 @@ class CharacterSystem;
|
||||
class CellGridSystem;
|
||||
class RoomLayoutSystem;
|
||||
class StartupMenuSystem;
|
||||
class DialogueSystem;
|
||||
class PlayerControllerSystem;
|
||||
class BuoyancySystem;
|
||||
class EditorSunSystem;
|
||||
@@ -41,6 +43,7 @@ class PathFollowingSystem;
|
||||
class GoapPlannerSystem;
|
||||
class ActuatorSystem;
|
||||
class EventHandlerSystem;
|
||||
class ItemSystem;
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
@@ -187,6 +190,10 @@ public:
|
||||
{
|
||||
return m_startupMenuSystem.get();
|
||||
}
|
||||
DialogueSystem *getDialogueSystem() const
|
||||
{
|
||||
return m_dialogueSystem.get();
|
||||
}
|
||||
ActuatorSystem *getActuatorSystem() const
|
||||
{
|
||||
return m_actuatorSystem.get();
|
||||
@@ -240,10 +247,11 @@ private:
|
||||
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
|
||||
@@ -254,6 +262,9 @@ private:
|
||||
bool m_setupComplete = false;
|
||||
bool m_debugBuoyancy = false;
|
||||
|
||||
// Lua scripting
|
||||
editScene::LuaState m_lua;
|
||||
|
||||
// Editor visualization nodes
|
||||
Ogre::SceneNode *m_gridNode = nullptr;
|
||||
Ogre::SceneNode *m_axisNode = nullptr;
|
||||
|
||||
@@ -32,6 +32,24 @@
|
||||
* 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"
|
||||
*/
|
||||
struct BehaviorTreeNode {
|
||||
Ogre::String type = "task";
|
||||
@@ -74,7 +92,10 @@ struct BehaviorTreeNode {
|
||||
type == "checkValue" || type == "blackboardDump" ||
|
||||
type == "delay" || type == "teleportToChild" ||
|
||||
type == "disablePhysics" || type == "enablePhysics" ||
|
||||
type == "sendEvent";
|
||||
type == "sendEvent" || type == "hasItem" ||
|
||||
type == "hasItemByName" || type == "countItem" ||
|
||||
type == "pickupItem" || type == "dropItem" ||
|
||||
type == "useItem" || type == "addItemToInventory";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
131
src/features/editScene/components/DialogueComponent.hpp
Normal file
131
src/features/editScene/components/DialogueComponent.hpp
Normal file
@@ -0,0 +1,131 @@
|
||||
#ifndef EDITSCENE_DIALOGUE_COMPONENT_HPP
|
||||
#define EDITSCENE_DIALOGUE_COMPONENT_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Visual-novel style dialogue box component.
|
||||
*
|
||||
* Displays a narration text box at the bottom of the screen with optional
|
||||
* player choices. The dialogue can be driven via the EventBus system
|
||||
* (using "dialogue_show" event) or directly via the component API.
|
||||
*
|
||||
* Only active in game mode (GamePlayState::Playing).
|
||||
*
|
||||
* Event payload (GoapBlackboard) parameters:
|
||||
* "text" (string) - Narration text to display
|
||||
* "choices" (string) - Comma-separated list of choice labels
|
||||
* "speaker" (string) - Optional speaker name
|
||||
* "auto_progress" (int) - If 1, clicking anywhere progresses (no choices)
|
||||
*
|
||||
* Component state transitions:
|
||||
* Idle -> Showing (on show() or event)
|
||||
* Showing -> AwaitingChoice (if choices provided)
|
||||
* Showing -> Idle (if no choices, on click progress)
|
||||
* AwaitingChoice -> Idle (on choice selected)
|
||||
*/
|
||||
struct DialogueComponent {
|
||||
/** Current state of the dialogue box */
|
||||
enum class State {
|
||||
Idle, ///< No dialogue active
|
||||
Showing, ///< Text is being displayed
|
||||
AwaitingChoice ///< Waiting for player to pick a choice
|
||||
};
|
||||
|
||||
State state = State::Idle;
|
||||
|
||||
/** The narration text to display */
|
||||
Ogre::String text;
|
||||
|
||||
/** Optional speaker name (displayed above the text) */
|
||||
Ogre::String speaker;
|
||||
|
||||
/** Player choice labels (empty = no choices, click to progress) */
|
||||
std::vector<Ogre::String> choices;
|
||||
|
||||
/** Font configuration */
|
||||
Ogre::String fontName = "Jupiteroid-Regular.ttf";
|
||||
float fontSize = 24.0f;
|
||||
|
||||
/** Speaker name font size (slightly smaller) */
|
||||
float speakerFontSize = 20.0f;
|
||||
|
||||
/** Background opacity (0.0 - 1.0) */
|
||||
float backgroundOpacity = 0.85f;
|
||||
|
||||
/** Height of the dialogue box as fraction of screen height (0.0 - 1.0) */
|
||||
float boxHeightFraction = 0.25f;
|
||||
|
||||
/** Vertical position as fraction from top (0.0 = top, 0.75 = bottom quarter) */
|
||||
float boxPositionFraction = 0.75f;
|
||||
|
||||
/** Whether the dialogue box is enabled (can be toggled) */
|
||||
bool enabled = true;
|
||||
|
||||
/** Callback invoked when a choice is selected (choice index, 1-based) */
|
||||
std::function<void(int)> onChoiceSelected;
|
||||
|
||||
/** Callback invoked when dialogue is dismissed (no choices mode) */
|
||||
std::function<void()> onDismissed;
|
||||
|
||||
/** Callback invoked when dialogue starts showing */
|
||||
std::function<void()> onShow;
|
||||
|
||||
/* --- API --- */
|
||||
|
||||
/** Show dialogue with given text and optional choices */
|
||||
void show(const Ogre::String &narrationText,
|
||||
const std::vector<Ogre::String> &choiceLabels = {},
|
||||
const Ogre::String &speakerName = "")
|
||||
{
|
||||
text = narrationText;
|
||||
choices = choiceLabels;
|
||||
speaker = speakerName;
|
||||
state = choices.empty() ? State::Showing :
|
||||
State::AwaitingChoice;
|
||||
if (onShow)
|
||||
onShow();
|
||||
}
|
||||
|
||||
/** Progress the dialogue (click-through when no choices) */
|
||||
void progress()
|
||||
{
|
||||
if (state == State::Showing && choices.empty()) {
|
||||
state = State::Idle;
|
||||
if (onDismissed)
|
||||
onDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
/** Select a choice by 1-based index */
|
||||
void selectChoice(int index)
|
||||
{
|
||||
if (state == State::AwaitingChoice && index >= 1 &&
|
||||
index <= (int)choices.size()) {
|
||||
state = State::Idle;
|
||||
if (onChoiceSelected)
|
||||
onChoiceSelected(index);
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if dialogue is currently active */
|
||||
bool isActive() const
|
||||
{
|
||||
return state != State::Idle;
|
||||
}
|
||||
|
||||
/** Reset dialogue to idle state */
|
||||
void reset()
|
||||
{
|
||||
state = State::Idle;
|
||||
text.clear();
|
||||
choices.clear();
|
||||
speaker.clear();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DIALOGUE_COMPONENT_HPP
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/DialogueEditor.hpp"
|
||||
#include "DialogueComponent.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Dialogue Box", "Game", DialogueComponent,
|
||||
DialogueEditor)
|
||||
{
|
||||
registry.registerComponent<DialogueComponent>(
|
||||
DialogueComponent_name, DialogueComponent_group,
|
||||
std::make_unique<DialogueEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<DialogueComponent>()) {
|
||||
e.set<DialogueComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<DialogueComponent>()) {
|
||||
e.remove<DialogueComponent>();
|
||||
}
|
||||
});
|
||||
}
|
||||
165
src/features/editScene/components/Inventory.hpp
Normal file
165
src/features/editScene/components/Inventory.hpp
Normal file
@@ -0,0 +1,165 @@
|
||||
#ifndef EDITSCENE_INVENTORY_HPP
|
||||
#define EDITSCENE_INVENTORY_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* A single slot in an inventory.
|
||||
* Stores a reference to an item entity (if the item is a world entity)
|
||||
* or stores item data directly for items that exist only in inventory.
|
||||
*/
|
||||
struct InventorySlot {
|
||||
// Flecs entity ID of the item (0 if slot is empty)
|
||||
flecs::entity_t itemEntity = 0;
|
||||
|
||||
// Item data for items that exist only in inventory (no world entity)
|
||||
Ogre::String itemId;
|
||||
Ogre::String itemName;
|
||||
Ogre::String itemType;
|
||||
int stackSize = 0;
|
||||
int maxStackSize = 99;
|
||||
float weight = 0.1f;
|
||||
int value = 1;
|
||||
Ogre::String useActionName;
|
||||
|
||||
bool isEmpty() const
|
||||
{
|
||||
return itemEntity == 0 && stackSize <= 0;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
itemEntity = 0;
|
||||
itemId.clear();
|
||||
itemName.clear();
|
||||
itemType.clear();
|
||||
stackSize = 0;
|
||||
maxStackSize = 99;
|
||||
weight = 0.1f;
|
||||
value = 1;
|
||||
useActionName.clear();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Inventory component.
|
||||
*
|
||||
* Attached to a character entity to hold items.
|
||||
* Can also be attached to container entities (chests, barrels, etc.)
|
||||
* to define their contents.
|
||||
*
|
||||
* The inventory stores items as InventorySlot entries, each of which
|
||||
* may reference a world ItemComponent entity or hold item data directly.
|
||||
*/
|
||||
struct InventoryComponent {
|
||||
// Maximum number of slots
|
||||
int maxSlots = 20;
|
||||
|
||||
// Current slots
|
||||
std::vector<InventorySlot> slots;
|
||||
|
||||
// Total weight of all items (computed)
|
||||
float totalWeight = 0.0f;
|
||||
|
||||
// Maximum weight capacity (0 = unlimited)
|
||||
float maxWeight = 50.0f;
|
||||
|
||||
// Whether this inventory is a container (chest, barrel, etc.)
|
||||
// Containers can be opened by characters to transfer items.
|
||||
bool isContainer = false;
|
||||
|
||||
// Whether this inventory is currently open (for containers being browsed)
|
||||
bool isOpen = false;
|
||||
|
||||
InventoryComponent() = default;
|
||||
|
||||
explicit InventoryComponent(int maxSlots_)
|
||||
: maxSlots(maxSlots_)
|
||||
{
|
||||
slots.reserve(maxSlots_);
|
||||
}
|
||||
|
||||
/** Find the first empty slot index, or -1 if full. */
|
||||
int findEmptySlot() const
|
||||
{
|
||||
for (int i = 0; i < maxSlots; i++) {
|
||||
if (i >= (int)slots.size())
|
||||
return i;
|
||||
if (slots[i].isEmpty())
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Find a slot containing an item with the given itemId. */
|
||||
int findItem(const Ogre::String &itemId) const
|
||||
{
|
||||
for (int i = 0; i < (int)slots.size(); i++) {
|
||||
if (!slots[i].isEmpty() && slots[i].itemId == itemId)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Find a slot containing an item with the given itemName. */
|
||||
int findItemByName(const Ogre::String &itemName) const
|
||||
{
|
||||
for (int i = 0; i < (int)slots.size(); i++) {
|
||||
if (!slots[i].isEmpty() &&
|
||||
slots[i].itemName == itemName)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Count total number of items (sum of stack sizes). */
|
||||
int countItems() const
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty())
|
||||
count += slot.stackSize;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Count how many of a specific itemId are in the inventory. */
|
||||
int countItem(const Ogre::String &itemId) const
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty() && slot.itemId == itemId)
|
||||
count += slot.stackSize;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Check if inventory has at least one of a specific itemId. */
|
||||
bool hasItem(const Ogre::String &itemId) const
|
||||
{
|
||||
return findItem(itemId) >= 0;
|
||||
}
|
||||
|
||||
/** Check if inventory has at least one of a specific itemName. */
|
||||
bool hasItemByName(const Ogre::String &itemName) const
|
||||
{
|
||||
return findItemByName(itemName) >= 0;
|
||||
}
|
||||
|
||||
/** Recalculate total weight. */
|
||||
void recalculateWeight()
|
||||
{
|
||||
totalWeight = 0.0f;
|
||||
for (const auto &slot : slots) {
|
||||
if (!slot.isEmpty())
|
||||
totalWeight += slot.weight * slot.stackSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_INVENTORY_HPP
|
||||
20
src/features/editScene/components/InventoryModule.cpp
Normal file
20
src/features/editScene/components/InventoryModule.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "Inventory.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/InventoryEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Inventory", "Game", InventoryComponent,
|
||||
InventoryEditor)
|
||||
{
|
||||
registry.registerComponent<InventoryComponent>(
|
||||
"Inventory", "Game", std::make_unique<InventoryEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<InventoryComponent>())
|
||||
e.set<InventoryComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<InventoryComponent>())
|
||||
e.remove<InventoryComponent>();
|
||||
});
|
||||
}
|
||||
59
src/features/editScene/components/Item.hpp
Normal file
59
src/features/editScene/components/Item.hpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifndef EDITSCENE_ITEM_HPP
|
||||
#define EDITSCENE_ITEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Item definition component.
|
||||
*
|
||||
* Attached to a world entity that represents a pickable item.
|
||||
* The ActuatorSystem detects items (entities with ItemComponent)
|
||||
* and shows "E - Pick up [ItemName]" prompts to the player.
|
||||
*
|
||||
* Items can also be placed in containers (chests, etc.) which
|
||||
* have an InventoryComponent.
|
||||
*
|
||||
* For AI characters, behavior tree nodes (hasItem, pickupItem,
|
||||
* dropItem, useItem, addItemToInventory) provide inventory access.
|
||||
*/
|
||||
struct ItemComponent {
|
||||
// Display name of the item (e.g. "Apple", "Sword", "Key")
|
||||
Ogre::String itemName = "Item";
|
||||
|
||||
// Item type for categorization (e.g. "food", "weapon", "key", "quest")
|
||||
Ogre::String itemType = "misc";
|
||||
|
||||
// Unique identifier for this item definition
|
||||
// Multiple entities can share the same itemId (e.g. multiple coins)
|
||||
Ogre::String itemId;
|
||||
|
||||
// Stack size: how many of this item are in this stack
|
||||
int stackSize = 1;
|
||||
|
||||
// Maximum stack size (0 = no stacking)
|
||||
int maxStackSize = 99;
|
||||
|
||||
// Weight per unit (for encumbrance calculations)
|
||||
float weight = 0.1f;
|
||||
|
||||
// Value (for trading)
|
||||
int value = 1;
|
||||
|
||||
// Name of the GOAP action to execute when "using" this item
|
||||
// (e.g. "eat", "equip", "read"). Empty = no use action.
|
||||
Ogre::String useActionName;
|
||||
|
||||
ItemComponent() = default;
|
||||
|
||||
explicit ItemComponent(const Ogre::String &name,
|
||||
const Ogre::String &type = "misc")
|
||||
: itemName(name)
|
||||
, itemType(type)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ITEM_HPP
|
||||
19
src/features/editScene/components/ItemModule.cpp
Normal file
19
src/features/editScene/components/ItemModule.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "Item.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/ItemEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Item", "Game", ItemComponent, ItemEditor)
|
||||
{
|
||||
registry.registerComponent<ItemComponent>(
|
||||
"Item", "Game", std::make_unique<ItemEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ItemComponent>())
|
||||
e.set<ItemComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ItemComponent>())
|
||||
e.remove<ItemComponent>();
|
||||
});
|
||||
}
|
||||
1931
src/features/editScene/lua/LuaComponentApi.cpp
Normal file
1931
src/features/editScene/lua/LuaComponentApi.cpp
Normal file
File diff suppressed because it is too large
Load Diff
55
src/features/editScene/lua/LuaComponentApi.hpp
Normal file
55
src/features/editScene/lua/LuaComponentApi.hpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#ifndef EDITSCENE_LUA_COMPONENT_API_HPP
|
||||
#define EDITSCENE_LUA_COMPONENT_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
/**
|
||||
* @file LuaComponentApi.hpp
|
||||
* @brief Lua API for adding, removing, and modifying ECS components.
|
||||
*
|
||||
* Provides a generic component API where Lua scripts can add, get, set,
|
||||
* and remove components on entities. Each component type is registered
|
||||
* with a name string and a set of field accessors.
|
||||
*
|
||||
* Exposed Lua globals (in the "ecs" table):
|
||||
* ecs.add_component(id, "ComponentName") -> nil
|
||||
* ecs.remove_component(id, "ComponentName") -> nil
|
||||
* ecs.has_component(id, "ComponentName") -> bool
|
||||
* ecs.get_component(id, "ComponentName") -> table (field -> value)
|
||||
* ecs.set_component(id, "ComponentName", table) -> nil
|
||||
* ecs.get_field(id, "ComponentName", "fieldName") -> value
|
||||
* ecs.set_field(id, "ComponentName", "fieldName", value) -> nil
|
||||
*
|
||||
* Supported component names (case-sensitive):
|
||||
* "EntityName", "Transform", "Renderable", "Light", "Camera",
|
||||
* "RigidBody", "PhysicsCollider", "Character", "CharacterSlots",
|
||||
* "AnimationTree", "AnimationTreeTemplate", "BehaviorTree",
|
||||
* "GoapBlackboard", "GoapAction", "GoapGoal", "ActionDatabase",
|
||||
* "ActionDebug", "SmartObject", "Actuator", "EventHandler",
|
||||
* "GoapPlanner", "GoapRunner", "PathFollowing", "NavMesh",
|
||||
* "NavMeshGeometrySource", "NavMeshAgent", "Item", "Inventory",
|
||||
* "Lod", "LodSettings", "StaticGeometry", "StaticGeometryMember",
|
||||
* "ProceduralTexture", "ProceduralMaterial", "Primitive",
|
||||
* "TriangleBuffer", "Sun", "Skybox", "WaterPlane", "WaterPhysics",
|
||||
* "BuoyancyInfo", "StartupMenu", "Dialogue", "PlayerController",
|
||||
* "CellGrid", "Room", "ClearArea", "Roof", "Lot", "District",
|
||||
* "Town", "FurnitureTemplate", "PrefabInstance", "EditorMarker"
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register all component Lua API functions into the "ecs" global table.
|
||||
*
|
||||
* Adds functions for component manipulation (add, remove, has, get, set,
|
||||
* get_field, set_field) to the existing "ecs" table.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaComponentApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_COMPONENT_API_HPP
|
||||
236
src/features/editScene/lua/LuaEntityApi.cpp
Normal file
236
src/features/editScene/lua/LuaEntityApi.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#include "LuaEntityApi.hpp"
|
||||
#include "components/EditorMarker.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <iostream>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// Global entity ID map
|
||||
LuaEntityIdMap g_luaEntityIdMap;
|
||||
|
||||
int luaEntityToId(flecs::entity e)
|
||||
{
|
||||
if (!e.is_valid())
|
||||
return -1;
|
||||
return g_luaEntityIdMap.addEntity(e);
|
||||
}
|
||||
|
||||
flecs::entity luaIdToEntity(int id)
|
||||
{
|
||||
return g_luaEntityIdMap.getEntity(id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: get the Flecs world from the Lua registry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static flecs::world getWorld(lua_State *L)
|
||||
{
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld");
|
||||
OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered");
|
||||
flecs::world *world =
|
||||
static_cast<flecs::world *>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return *world;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.create_entity() -> int (entity ID)
|
||||
// Creates a new entity with EditorMarkerComponent and returns its Lua ID.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaCreateEntity(lua_State *L)
|
||||
{
|
||||
flecs::world world = getWorld(L);
|
||||
flecs::entity e = world.entity();
|
||||
// Add EditorMarkerComponent so it appears in the editor hierarchy
|
||||
// (the component is forward-declared; we use a tag approach)
|
||||
e.add<EditorMarkerComponent>();
|
||||
int id = g_luaEntityIdMap.addEntity(e);
|
||||
lua_pushinteger(L, id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.destroy_entity(id) -> nil
|
||||
// Destroys an entity and removes it from the ID map.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaDestroyEntity(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
if (e.is_alive()) {
|
||||
g_luaEntityIdMap.removeEntity(e);
|
||||
e.destruct();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.entity_exists(id) -> bool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaEntityExists(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
lua_pushboolean(L, g_luaEntityIdMap.hasId(id) ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.get_player_entity() -> int (entity ID) or nil
|
||||
// Looks up the entity named "player" in the Flecs world.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetPlayerEntity(lua_State *L)
|
||||
{
|
||||
flecs::world world = getWorld(L);
|
||||
flecs::entity e = world.lookup("player");
|
||||
if (e.is_valid()) {
|
||||
int id = g_luaEntityIdMap.addEntity(e);
|
||||
lua_pushinteger(L, id);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.get_entity_by_name(name) -> int (entity ID) or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetEntityByName(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TSTRING);
|
||||
const char *name = lua_tostring(L, 1);
|
||||
flecs::world world = getWorld(L);
|
||||
flecs::entity e = world.lookup(name);
|
||||
if (e.is_valid()) {
|
||||
int id = g_luaEntityIdMap.addEntity(e);
|
||||
lua_pushinteger(L, id);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.set_entity_name(id, name) -> nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaSetEntityName(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
luaL_checktype(L, 2, LUA_TSTRING);
|
||||
int id = lua_tointeger(L, 1);
|
||||
const char *name = lua_tostring(L, 2);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
e.set_name(name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.get_entity_name(id) -> string or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetEntityName(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
const char *name = e.name();
|
||||
if (name && name[0]) {
|
||||
lua_pushstring(L, name);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.parent(id) -> int (parent ID) or nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetParent(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
flecs::entity parent = e.parent();
|
||||
if (parent.is_valid()) {
|
||||
int parentId = g_luaEntityIdMap.addEntity(parent);
|
||||
lua_pushinteger(L, parentId);
|
||||
} else {
|
||||
lua_pushnil(L);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.children(id) -> table of child IDs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetChildren(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TNUMBER);
|
||||
int id = lua_tointeger(L, 1);
|
||||
flecs::entity e = g_luaEntityIdMap.getEntity(id);
|
||||
|
||||
lua_newtable(L); // result table
|
||||
int index = 1;
|
||||
e.children([&](flecs::entity child) {
|
||||
int childId = g_luaEntityIdMap.addEntity(child);
|
||||
lua_pushinteger(L, childId);
|
||||
lua_rawseti(L, -2, index);
|
||||
index++;
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register all entity API functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaEntityApi(lua_State *L)
|
||||
{
|
||||
// Create the "ecs" global table
|
||||
lua_newtable(L);
|
||||
|
||||
// Entity management
|
||||
lua_pushcfunction(L, luaCreateEntity);
|
||||
lua_setfield(L, -2, "create_entity");
|
||||
|
||||
lua_pushcfunction(L, luaDestroyEntity);
|
||||
lua_setfield(L, -2, "destroy_entity");
|
||||
|
||||
lua_pushcfunction(L, luaEntityExists);
|
||||
lua_setfield(L, -2, "entity_exists");
|
||||
|
||||
lua_pushcfunction(L, luaGetPlayerEntity);
|
||||
lua_setfield(L, -2, "get_player_entity");
|
||||
|
||||
lua_pushcfunction(L, luaGetEntityByName);
|
||||
lua_setfield(L, -2, "get_entity_by_name");
|
||||
|
||||
lua_pushcfunction(L, luaSetEntityName);
|
||||
lua_setfield(L, -2, "set_entity_name");
|
||||
|
||||
lua_pushcfunction(L, luaGetEntityName);
|
||||
lua_setfield(L, -2, "get_entity_name");
|
||||
|
||||
// Hierarchy
|
||||
lua_pushcfunction(L, luaGetParent);
|
||||
lua_setfield(L, -2, "parent");
|
||||
|
||||
lua_pushcfunction(L, luaGetChildren);
|
||||
lua_setfield(L, -2, "children");
|
||||
|
||||
// Set the global
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
126
src/features/editScene/lua/LuaEntityApi.hpp
Normal file
126
src/features/editScene/lua/LuaEntityApi.hpp
Normal file
@@ -0,0 +1,126 @@
|
||||
#ifndef EDITSCENE_LUA_ENTITY_API_HPP
|
||||
#define EDITSCENE_LUA_ENTITY_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <lua.hpp>
|
||||
#include <Ogre.h>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* @file LuaEntityApi.hpp
|
||||
* @brief Lua API for entity creation, destruction, and ID mapping.
|
||||
*
|
||||
* Provides a bidirectional mapping between Lua integer IDs and
|
||||
* Flecs entity handles. Lua scripts reference entities by integer
|
||||
* IDs rather than raw Flecs handles for safety and simplicity.
|
||||
*
|
||||
* Exposed Lua globals:
|
||||
* ecs.create_entity() -> int (entity ID)
|
||||
* ecs.destroy_entity(id) -> nil
|
||||
* ecs.entity_exists(id) -> bool
|
||||
* ecs.get_player_entity() -> int (entity ID)
|
||||
* ecs.get_entity_by_name(name) -> int (entity ID) or nil
|
||||
* ecs.set_entity_name(id, name)-> nil
|
||||
* ecs.get_entity_name(id) -> string or nil
|
||||
* ecs.parent(id) -> int (parent ID) or nil
|
||||
* ecs.children(id) -> table of child IDs
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Global bidirectional mapping between Lua integer IDs and Flecs entities.
|
||||
*
|
||||
* This is a singleton-like global so that all Lua API functions can
|
||||
* access it without needing to pass it through every closure.
|
||||
*/
|
||||
struct LuaEntityIdMap {
|
||||
std::unordered_map<int, flecs::entity> id2entity;
|
||||
std::unordered_map<flecs::entity_t, int> entity2id;
|
||||
int nextId = 0;
|
||||
|
||||
/** @brief Get the next available integer ID. */
|
||||
int getNextId()
|
||||
{
|
||||
nextId++;
|
||||
return nextId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add an entity to the map, returning its integer ID.
|
||||
* If the entity is already mapped, returns the existing ID.
|
||||
*/
|
||||
int addEntity(flecs::entity e)
|
||||
{
|
||||
if (entity2id.find(e.id()) != entity2id.end())
|
||||
return entity2id[e.id()];
|
||||
int id = getNextId();
|
||||
id2entity[id] = e;
|
||||
entity2id[e.id()] = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the Flecs entity for an integer ID.
|
||||
* Asserts if the ID is not found or the entity is invalid.
|
||||
*/
|
||||
flecs::entity getEntity(int id)
|
||||
{
|
||||
auto it = id2entity.find(id);
|
||||
OgreAssert(it != id2entity.end(), "Invalid entity ID");
|
||||
OgreAssert(it->second.is_valid(), "Entity is no longer valid");
|
||||
return it->second;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Remove an entity from the map.
|
||||
*/
|
||||
void removeEntity(flecs::entity e)
|
||||
{
|
||||
auto it = entity2id.find(e.id());
|
||||
if (it != entity2id.end()) {
|
||||
id2entity.erase(it->second);
|
||||
entity2id.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if an integer ID is valid.
|
||||
*/
|
||||
bool hasId(int id) const
|
||||
{
|
||||
auto it = id2entity.find(id);
|
||||
return it != id2entity.end() && it->second.is_valid();
|
||||
}
|
||||
};
|
||||
|
||||
/** @brief Global entity ID map instance. */
|
||||
extern LuaEntityIdMap g_luaEntityIdMap;
|
||||
|
||||
/**
|
||||
* @brief Convert a Flecs entity to a Lua integer ID.
|
||||
* @return The integer ID, or -1 if the entity is invalid.
|
||||
*/
|
||||
int luaEntityToId(flecs::entity e);
|
||||
|
||||
/**
|
||||
* @brief Convert a Lua integer ID to a Flecs entity.
|
||||
* Asserts if the ID is invalid.
|
||||
*/
|
||||
flecs::entity luaIdToEntity(int id);
|
||||
|
||||
/**
|
||||
* @brief Register all entity-related Lua API functions into the global table.
|
||||
*
|
||||
* Creates the "ecs" global table (or adds to it) with entity management
|
||||
* functions. Must be called after LuaState is constructed.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaEntityApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_ENTITY_API_HPP
|
||||
320
src/features/editScene/lua/LuaEventApi.cpp
Normal file
320
src/features/editScene/lua/LuaEventApi.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
#include "LuaEventApi.hpp"
|
||||
#include "LuaEntityApi.hpp"
|
||||
#include "../systems/EventBus.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreVector3.h>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal: subscription ID tracking for Lua-managed subscriptions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Map from Lua subscription IDs to EventBus ListenerIds.
|
||||
*
|
||||
* When Lua subscribes to an event, we store the mapping so that
|
||||
* unsubscribe_event() can remove the correct listener.
|
||||
*/
|
||||
static std::unordered_map<int, EventBus::ListenerId> s_luaSubscriptions;
|
||||
static int s_nextLuaSubId = 1;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push a GoapBlackboard as a Lua table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Push a GoapBlackboard as a Lua table with named fields.
|
||||
*
|
||||
* The resulting table has:
|
||||
* .bits -> integer
|
||||
* .mask -> integer
|
||||
* .values -> {string -> int}
|
||||
* .floatValues -> {string -> float}
|
||||
* .vec3Values -> {string -> {x, y, z}}
|
||||
* .stringValues -> {string -> string}
|
||||
*
|
||||
* @param L Lua state.
|
||||
* @param bb The GoapBlackboard to convert.
|
||||
*/
|
||||
static void pushGoapBlackboard(lua_State *L, const GoapBlackboard &bb)
|
||||
{
|
||||
lua_newtable(L);
|
||||
|
||||
// bits and mask
|
||||
lua_pushinteger(L, (lua_Integer)bb.bits);
|
||||
lua_setfield(L, -2, "bits");
|
||||
|
||||
lua_pushinteger(L, (lua_Integer)bb.mask);
|
||||
lua_setfield(L, -2, "mask");
|
||||
|
||||
// values (int map)
|
||||
lua_newtable(L);
|
||||
for (auto &kv : bb.values) {
|
||||
lua_pushinteger(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "values");
|
||||
|
||||
// floatValues
|
||||
lua_newtable(L);
|
||||
for (auto &kv : bb.floatValues) {
|
||||
lua_pushnumber(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "floatValues");
|
||||
|
||||
// vec3Values
|
||||
lua_newtable(L);
|
||||
for (auto &kv : bb.vec3Values) {
|
||||
lua_newtable(L);
|
||||
lua_pushnumber(L, kv.second.x);
|
||||
lua_rawseti(L, -2, 1);
|
||||
lua_pushnumber(L, kv.second.y);
|
||||
lua_rawseti(L, -2, 2);
|
||||
lua_pushnumber(L, kv.second.z);
|
||||
lua_rawseti(L, -2, 3);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "vec3Values");
|
||||
|
||||
// stringValues
|
||||
lua_newtable(L);
|
||||
for (auto &kv : bb.stringValues) {
|
||||
lua_pushstring(L, kv.second.c_str());
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "stringValues");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read a Lua table at the given index as a GoapBlackboard.
|
||||
*
|
||||
* Expects the same format as pushGoapBlackboard produces.
|
||||
*
|
||||
* @param L Lua state.
|
||||
* @param idx Stack index of the table.
|
||||
* @return GoapBlackboard populated from the table.
|
||||
*/
|
||||
static GoapBlackboard readGoapBlackboard(lua_State *L, int idx)
|
||||
{
|
||||
GoapBlackboard bb;
|
||||
|
||||
// bits
|
||||
lua_getfield(L, idx, "bits");
|
||||
if (lua_isnumber(L, -1))
|
||||
bb.bits = (uint64_t)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// mask
|
||||
lua_getfield(L, idx, "mask");
|
||||
if (lua_isnumber(L, -1))
|
||||
bb.mask = (uint64_t)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// values (int map)
|
||||
lua_getfield(L, idx, "values");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isnumber(L, -1))
|
||||
bb.values[lua_tostring(L, -2)] =
|
||||
(int)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// floatValues
|
||||
lua_getfield(L, idx, "floatValues");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isnumber(L, -1))
|
||||
bb.floatValues[lua_tostring(L, -2)] =
|
||||
(float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// vec3Values
|
||||
lua_getfield(L, idx, "vec3Values");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_istable(L, -1)) {
|
||||
Ogre::Vector3 v;
|
||||
lua_rawgeti(L, -1, 1);
|
||||
v.x = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_rawgeti(L, -1, 2);
|
||||
v.y = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_rawgeti(L, -1, 3);
|
||||
v.z = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
bb.vec3Values[lua_tostring(L, -2)] = v;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// stringValues
|
||||
lua_getfield(L, idx, "stringValues");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isstring(L, -1))
|
||||
bb.stringValues[lua_tostring(L, -2)] =
|
||||
lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
return bb;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.send_event("eventName", [params_table])
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Lua: ecs.send_event("eventName", [params_table]) -> nil
|
||||
*
|
||||
* Sends an event through the global EventBus. The optional params table
|
||||
* is converted to a GoapBlackboard payload.
|
||||
*
|
||||
* Usage:
|
||||
* ecs.send_event("collision")
|
||||
* ecs.send_event("collision", { entity_id = 42, damage = 10 })
|
||||
*/
|
||||
static int luaSendEvent(lua_State *L)
|
||||
{
|
||||
const char *eventName = luaL_checkstring(L, 1);
|
||||
|
||||
GoapBlackboard params;
|
||||
if (lua_gettop(L) >= 2 && lua_istable(L, 2)) {
|
||||
params = readGoapBlackboard(L, 2);
|
||||
}
|
||||
|
||||
EventBus::getInstance().send(eventName, params);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.subscribe_event("eventName", callback_fn) -> int (subscription id)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Lua: ecs.subscribe_event("eventName", callback_fn) -> int
|
||||
*
|
||||
* Subscribes a Lua function to an event. The callback receives:
|
||||
* function(event_name, params_table)
|
||||
*
|
||||
* Returns a subscription ID that can be used with unsubscribe_event().
|
||||
*
|
||||
* Usage:
|
||||
* local sub_id = ecs.subscribe_event("collision", function(event, params)
|
||||
* print("Collision event received!")
|
||||
* print("entity_id: " .. params.values.entity_id)
|
||||
* end)
|
||||
*/
|
||||
static int luaSubscribeEvent(lua_State *L)
|
||||
{
|
||||
const char *eventName = luaL_checkstring(L, 1);
|
||||
luaL_checktype(L, 2, LUA_TFUNCTION);
|
||||
|
||||
// Create a reference to the Lua callback function
|
||||
lua_pushvalue(L, 2);
|
||||
int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
|
||||
// Subscribe to the EventBus with a C++ lambda that calls the Lua function
|
||||
EventBus::ListenerId listenerId = EventBus::getInstance().subscribe(
|
||||
eventName, [L, callbackRef](const Ogre::String &eventName,
|
||||
const GoapBlackboard ¶ms) {
|
||||
// Push the Lua callback function
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef);
|
||||
|
||||
// Push event name
|
||||
lua_pushstring(L, eventName.c_str());
|
||||
|
||||
// Push params as a Lua table
|
||||
pushGoapBlackboard(L, params);
|
||||
|
||||
// Call the Lua function (2 args, 0 results)
|
||||
if (lua_pcall(L, 2, 0, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "Lua event callback error: "
|
||||
<< lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Store the mapping from Lua subscription ID to EventBus listener ID
|
||||
int luaSubId = s_nextLuaSubId++;
|
||||
s_luaSubscriptions[luaSubId] = listenerId;
|
||||
|
||||
lua_pushinteger(L, luaSubId);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.unsubscribe_event(subscription_id) -> nil
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Lua: ecs.unsubscribe_event(subscription_id) -> nil
|
||||
*
|
||||
* Unsubscribes a previously registered event subscription.
|
||||
*
|
||||
* Usage:
|
||||
* ecs.unsubscribe_event(sub_id)
|
||||
*/
|
||||
static int luaUnsubscribeEvent(lua_State *L)
|
||||
{
|
||||
int luaSubId = (int)luaL_checkinteger(L, 1);
|
||||
|
||||
auto it = s_luaSubscriptions.find(luaSubId);
|
||||
if (it != s_luaSubscriptions.end()) {
|
||||
EventBus::getInstance().unsubscribe(it->second);
|
||||
s_luaSubscriptions.erase(it);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public registration function
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaEventApi(lua_State *L)
|
||||
{
|
||||
// Get or create the "ecs" global table
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_newtable(L);
|
||||
lua_setglobal(L, "ecs");
|
||||
lua_getglobal(L, "ecs");
|
||||
}
|
||||
|
||||
// Register event functions
|
||||
lua_pushcfunction(L, luaSendEvent);
|
||||
lua_setfield(L, -2, "send_event");
|
||||
|
||||
lua_pushcfunction(L, luaSubscribeEvent);
|
||||
lua_setfield(L, -2, "subscribe_event");
|
||||
|
||||
lua_pushcfunction(L, luaUnsubscribeEvent);
|
||||
lua_setfield(L, -2, "unsubscribe_event");
|
||||
|
||||
// Pop the ecs table
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
55
src/features/editScene/lua/LuaEventApi.hpp
Normal file
55
src/features/editScene/lua/LuaEventApi.hpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#ifndef EDITSCENE_LUA_EVENT_API_HPP
|
||||
#define EDITSCENE_LUA_EVENT_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
/**
|
||||
* @file LuaEventApi.hpp
|
||||
* @brief Lua API for the EventBus system.
|
||||
*
|
||||
* Provides Lua bindings for the global EventBus singleton, allowing
|
||||
* Lua scripts to subscribe to events, send events, and manage
|
||||
* event subscriptions.
|
||||
*
|
||||
* The EventBus is a synchronous publish/subscribe system. Events are
|
||||
* identified by name strings. Payloads use GoapBlackboard, which
|
||||
* supports int, float, Vector3, and string values.
|
||||
*
|
||||
* Exposed Lua globals (in the "ecs" table):
|
||||
* ecs.send_event("eventName") -> nil
|
||||
* ecs.send_event("eventName", {key=value, ...}) -> nil
|
||||
* ecs.subscribe_event("eventName", callback_fn) -> int (subscription id)
|
||||
* ecs.unsubscribe_event(subscription_id) -> nil
|
||||
*
|
||||
* The callback function receives (event_name, params_table):
|
||||
* ecs.subscribe_event("collision", function(event, params)
|
||||
* print(event .. " occurred")
|
||||
* print("entity_id: " .. params.entity_id)
|
||||
* end)
|
||||
*
|
||||
* The params table contains the GoapBlackboard fields:
|
||||
* params.bits -> integer (bitfield)
|
||||
* params.mask -> integer (bitmask)
|
||||
* params.values -> table {string -> int}
|
||||
* params.floatValues -> table {string -> float}
|
||||
* params.vec3Values -> table {string -> {x, y, z}}
|
||||
* params.stringValues -> table {string -> string}
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register all event-related Lua API functions into the "ecs" table.
|
||||
*
|
||||
* Adds functions for event subscription, unsubscription, and sending.
|
||||
* Must be called after LuaState is constructed and the "ecs" table exists.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaEventApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_EVENT_API_HPP
|
||||
187
src/features/editScene/lua/LuaState.cpp
Normal file
187
src/features/editScene/lua/LuaState.cpp
Normal file
@@ -0,0 +1,187 @@
|
||||
#include "LuaState.hpp"
|
||||
#include <OgreResourceGroupManager.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreDataStream.h>
|
||||
#include <iostream>
|
||||
|
||||
extern "C" {
|
||||
int luaopen_lpeg(lua_State *L);
|
||||
}
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom library loader: loads .lua files from OGRE resource groups
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int LuaState::luaLibraryLoader(lua_State *L)
|
||||
{
|
||||
if (!lua_isstring(L, 1)) {
|
||||
luaL_error(
|
||||
L,
|
||||
"luaLibraryLoader: Expected string for first parameter");
|
||||
}
|
||||
|
||||
std::string libraryFile = lua_tostring(L, 1);
|
||||
|
||||
// Translate '.' to '/' for OGRE resource path compatibility
|
||||
while (libraryFile.find('.') != std::string::npos)
|
||||
libraryFile.replace(libraryFile.find('.'), 1, "/");
|
||||
|
||||
libraryFile += ".lua";
|
||||
|
||||
Ogre::DataStreamPtr stream =
|
||||
Ogre::ResourceGroupManager::getSingleton().openResource(
|
||||
libraryFile, "LuaScripts");
|
||||
Ogre::String script = stream->getAsString();
|
||||
if (luaL_loadbuffer(L, script.c_str(), script.length(),
|
||||
libraryFile.c_str())) {
|
||||
luaL_error(
|
||||
L,
|
||||
"Error loading library '%s' from resource archive.\n%s",
|
||||
libraryFile.c_str(), lua_tostring(L, -1));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Install the custom loader into package.searchers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void LuaState::installLibraryLoader()
|
||||
{
|
||||
lua_getglobal(L, "table");
|
||||
lua_getfield(L, -1, "insert");
|
||||
lua_remove(L, -2); // table
|
||||
lua_getglobal(L, "package");
|
||||
lua_getfield(L, -1, "searchers");
|
||||
lua_remove(L, -2); // package
|
||||
lua_pushnumber(L, 1); // insert at position 1 (highest priority)
|
||||
lua_pushcfunction(L, luaLibraryLoader);
|
||||
if (lua_pcall(L, 3, 0, 0))
|
||||
Ogre::LogManager::getSingleton().stream() << lua_tostring(L, 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructor: create Lua state and open standard libraries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LuaState::LuaState()
|
||||
: L(luaL_newstate())
|
||||
{
|
||||
luaopen_base(L);
|
||||
luaopen_table(L);
|
||||
luaopen_package(L);
|
||||
luaL_requiref(L, "table", luaopen_table, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, "math", luaopen_math, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, "package", luaopen_package, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, "string", luaopen_string, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, "io", luaopen_io, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, "lpeg", luaopen_lpeg, 1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
installLibraryLoader();
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Destructor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LuaState::~LuaState()
|
||||
{
|
||||
lua_close(L);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handler registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int LuaState::setupHandler()
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TFUNCTION);
|
||||
lua_pushvalue(L, 1);
|
||||
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
setupHandlers.push_back(ref);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Call all handlers with an event name (no entities)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int LuaState::callHandler(const Ogre::String &event)
|
||||
{
|
||||
for (int ref : setupHandlers) {
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||
lua_pushstring(L, event.c_str());
|
||||
lua_pushinteger(L, -1);
|
||||
lua_pushinteger(L, -1);
|
||||
if (lua_pcall(L, 3, 0, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< lua_tostring(L, -1);
|
||||
OgreAssert(false, "Lua error");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Call all handlers with an event name and two entities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int LuaState::callHandler(const Ogre::String &event, flecs::entity e1,
|
||||
flecs::entity e2)
|
||||
{
|
||||
// Entity IDs are mapped through the global idmap (see LuaEntityApi.cpp)
|
||||
extern int luaEntityToId(flecs::entity e);
|
||||
extern flecs::entity luaIdToEntity(int id);
|
||||
|
||||
for (int ref : setupHandlers) {
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||
lua_pushstring(L, event.c_str());
|
||||
lua_pushinteger(L, luaEntityToId(e1));
|
||||
lua_pushinteger(L, luaEntityToId(e2));
|
||||
if (lua_pcall(L, 3, 0, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< lua_tostring(L, -1);
|
||||
OgreAssert(false, "Lua error");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Late setup: load data.lua and run initialisation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void LuaState::lateSetup()
|
||||
{
|
||||
Ogre::DataStreamPtr stream =
|
||||
Ogre::ResourceGroupManager::getSingleton().openResource(
|
||||
"data.lua", "LuaScripts");
|
||||
std::cout << "stream: " << stream->getAsString() << "\n";
|
||||
if (luaL_dostring(L, stream->getAsString().c_str()) != LUA_OK) {
|
||||
std::cout << "error: " << lua_tostring(L, -1) << "\n";
|
||||
OgreAssert(false, "Script failure");
|
||||
}
|
||||
|
||||
const char *lua_code = "\n\
|
||||
function stuff()\n\
|
||||
return 4\n\
|
||||
end\n\
|
||||
x = stuff()\n\
|
||||
";
|
||||
luaL_dostring(L, lua_code);
|
||||
lua_getglobal(L, "x");
|
||||
int x = lua_tonumber(L, 1);
|
||||
std::cout << "lua: " << x << "\n";
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
137
src/features/editScene/lua/LuaState.hpp
Normal file
137
src/features/editScene/lua/LuaState.hpp
Normal file
@@ -0,0 +1,137 @@
|
||||
#ifndef EDITSCENE_LUA_STATE_HPP
|
||||
#define EDITSCENE_LUA_STATE_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <lua.hpp>
|
||||
#include <flecs.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @file LuaState.hpp
|
||||
* @brief Lua state management for the editScene editor.
|
||||
*
|
||||
* Manages the Lua virtual machine instance, library loading,
|
||||
* script loading from OGRE resource groups, and the custom
|
||||
* package.searchers entry that loads Lua modules from
|
||||
* the "LuaScripts" resource group.
|
||||
*
|
||||
* Usage:
|
||||
* LuaState lua;
|
||||
* lua.installLibraryLoader(); // Register custom searcher
|
||||
* lua.lateSetup(); // Load data.lua and run startup
|
||||
* lua.callHandler("event_name", e1, e2);
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Manages a single lua_State instance.
|
||||
*
|
||||
* Opens standard Lua libraries (base, table, math, package, string, io)
|
||||
* plus LPEG. Installs a custom package.searchers entry that loads
|
||||
* .lua files from OGRE's "LuaScripts" resource group.
|
||||
*
|
||||
* Provides a handler/callback system: Lua scripts can register
|
||||
* callback functions via setup_handler(), and C++ code can invoke
|
||||
* them with event names and optional entity parameters.
|
||||
*/
|
||||
class LuaState {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Lua state.
|
||||
*
|
||||
* Opens all standard libraries and installs the custom
|
||||
* library loader for OGRE resource-based Lua modules.
|
||||
*/
|
||||
LuaState();
|
||||
|
||||
/**
|
||||
* @brief Destroy the Lua state and close the VM.
|
||||
*/
|
||||
~LuaState();
|
||||
|
||||
/**
|
||||
* @brief Get the underlying lua_State pointer.
|
||||
*/
|
||||
lua_State *getState() const
|
||||
{
|
||||
return L;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Install the custom library loader into package.searchers.
|
||||
*
|
||||
* Inserts luaLibraryLoader at position 1 of the searchers table
|
||||
* so it takes precedence over the default file-system loader.
|
||||
* This allows Lua's require() to load modules from OGRE resource
|
||||
* archives.
|
||||
*/
|
||||
void installLibraryLoader();
|
||||
|
||||
/**
|
||||
* @brief Late setup: load data.lua and run initialisation.
|
||||
*
|
||||
* Opens "data.lua" from the "LuaScripts" resource group and
|
||||
* executes it. Also runs a simple inline Lua test snippet.
|
||||
* Called once after the ECS world is fully initialised.
|
||||
*/
|
||||
void lateSetup();
|
||||
|
||||
/**
|
||||
* @brief Register a Lua callback function for event handling.
|
||||
*
|
||||
* Expects a Lua function on the stack (at index 1).
|
||||
* Stores a reference to it in the registry so it can be
|
||||
* called later via callHandler().
|
||||
*
|
||||
* @return int Reference index (stored internally).
|
||||
*/
|
||||
int setupHandler();
|
||||
|
||||
/**
|
||||
* @brief Call all registered handlers with an event name.
|
||||
*
|
||||
* @param event The event name string.
|
||||
* @return int 0 on success.
|
||||
*/
|
||||
int callHandler(const Ogre::String &event);
|
||||
|
||||
/**
|
||||
* @brief Call all registered handlers with an event name and
|
||||
* two entity IDs.
|
||||
*
|
||||
* Entity IDs are mapped through the global idmap (see LuaEntityApi).
|
||||
*
|
||||
* @param event The event name string.
|
||||
* @param e1 First entity (e.g. subject).
|
||||
* @param e2 Second entity (e.g. object).
|
||||
* @return int 0 on success.
|
||||
*/
|
||||
int callHandler(const Ogre::String &event, flecs::entity e1,
|
||||
flecs::entity e2);
|
||||
|
||||
private:
|
||||
lua_State *L;
|
||||
|
||||
/** Registry references for registered Lua callback functions. */
|
||||
std::vector<int> setupHandlers;
|
||||
|
||||
/**
|
||||
* @brief Custom Lua loader function for OGRE resource-based modules.
|
||||
*
|
||||
* Registered in package.searchers. Translates dots to path
|
||||
* separators, appends ".lua", and loads the file from the
|
||||
* "LuaScripts" OGRE resource group.
|
||||
*
|
||||
* @param L Lua state.
|
||||
* @return int 1 (pushes loaded chunk onto stack) or error.
|
||||
*/
|
||||
static int luaLibraryLoader(lua_State *L);
|
||||
};
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_STATE_HPP
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "ActuatorSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "BehaviorTreeSystem.hpp"
|
||||
#include "ItemSystem.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include "../components/PlayerController.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
@@ -107,8 +110,7 @@ void ActuatorSystem::executeAction(flecs::entity character,
|
||||
"[ActuatorSystem] Executing action: " + actionName);
|
||||
}
|
||||
|
||||
bool ActuatorSystem::isActionComplete(flecs::entity character,
|
||||
float deltaTime)
|
||||
bool ActuatorSystem::isActionComplete(flecs::entity character, float deltaTime)
|
||||
{
|
||||
if (!m_btSystem || !character.is_alive())
|
||||
return true;
|
||||
@@ -131,9 +133,10 @@ bool ActuatorSystem::isActionComplete(flecs::entity character,
|
||||
return true;
|
||||
|
||||
// Evaluate the behavior tree directly (no ActionDebug)
|
||||
auto status = m_btSystem->evaluatePlayerAction(
|
||||
character.id(), action->behaviorTree, deltaTime,
|
||||
m_actionFirstFrame);
|
||||
auto status = m_btSystem->evaluatePlayerAction(character.id(),
|
||||
action->behaviorTree,
|
||||
deltaTime,
|
||||
m_actionFirstFrame);
|
||||
m_actionFirstFrame = false;
|
||||
|
||||
return status != BehaviorTreeSystem::Status::running;
|
||||
@@ -152,9 +155,9 @@ void ActuatorSystem::drawActionMenu(flecs::entity actuatorEntity)
|
||||
ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Appearing);
|
||||
|
||||
ImGuiWindowFlags flags =
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_AlwaysAutoResize;
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_AlwaysAutoResize;
|
||||
|
||||
if (ImGui::Begin("Select Action", nullptr, flags)) {
|
||||
ImGui::Text("Select an action:");
|
||||
@@ -163,9 +166,10 @@ void ActuatorSystem::drawActionMenu(flecs::entity actuatorEntity)
|
||||
for (const auto &name : actuator.actionNames) {
|
||||
if (name.empty())
|
||||
continue;
|
||||
if (ImGui::Button(name.c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x,
|
||||
0))) {
|
||||
if (ImGui::Button(
|
||||
name.c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x,
|
||||
0))) {
|
||||
m_pendingActionName = name;
|
||||
}
|
||||
}
|
||||
@@ -204,18 +208,21 @@ void ActuatorSystem::update(float deltaTime)
|
||||
|
||||
// Check if an action is currently executing
|
||||
if (m_executingActuatorId != 0) {
|
||||
flecs::entity character = m_world.entity(m_executingCharacterId);
|
||||
flecs::entity character =
|
||||
m_world.entity(m_executingCharacterId);
|
||||
if (isActionComplete(character, deltaTime)) {
|
||||
// Action finished - start cooldown
|
||||
flecs::entity actuator = m_world.entity(m_executingActuatorId);
|
||||
if (actuator.is_alive() && actuator.has<ActuatorComponent>()) {
|
||||
auto &ac = actuator.get_mut<ActuatorComponent>();
|
||||
flecs::entity actuator =
|
||||
m_world.entity(m_executingActuatorId);
|
||||
if (actuator.is_alive() &&
|
||||
actuator.has<ActuatorComponent>()) {
|
||||
auto &ac =
|
||||
actuator.get_mut<ActuatorComponent>();
|
||||
ac.isExecuting = false;
|
||||
float cooldown = 1.5f;
|
||||
m_world
|
||||
.query<PlayerControllerComponent>()
|
||||
.each([&](flecs::entity,
|
||||
PlayerControllerComponent &pc) {
|
||||
m_world.query<PlayerControllerComponent>().each(
|
||||
[&](flecs::entity,
|
||||
PlayerControllerComponent &pc) {
|
||||
cooldown = pc.actuatorCooldown;
|
||||
});
|
||||
ac.cooldownTimer = cooldown;
|
||||
@@ -243,52 +250,53 @@ void ActuatorSystem::update(float deltaTime)
|
||||
m_nearRadius = 14.0f;
|
||||
m_labelFontSize = 14.0f;
|
||||
|
||||
m_world.query<PlayerControllerComponent>().each(
|
||||
[&](flecs::entity e, PlayerControllerComponent &pc) {
|
||||
(void)e;
|
||||
if (!playerCharacter.is_alive()) {
|
||||
m_world.query<EntityNameComponent>()
|
||||
.each([&](flecs::entity ec,
|
||||
EntityNameComponent &en) {
|
||||
if (!playerCharacter.is_alive() &&
|
||||
en.name == pc.targetCharacterName)
|
||||
playerCharacter = ec;
|
||||
});
|
||||
actuatorDistance = pc.actuatorDistance;
|
||||
actuatorCooldown = pc.actuatorCooldown;
|
||||
m_circleColor = ImVec4(pc.actuatorColor.x,
|
||||
pc.actuatorColor.y,
|
||||
pc.actuatorColor.z,
|
||||
1.0f);
|
||||
m_distantRadius = pc.distantCircleRadius;
|
||||
m_nearRadius = pc.nearCircleRadius;
|
||||
m_labelFontSize = pc.actuatorLabelFontSize;
|
||||
}
|
||||
});
|
||||
m_world.query<PlayerControllerComponent>().each([&](flecs::entity e,
|
||||
PlayerControllerComponent
|
||||
&pc) {
|
||||
(void)e;
|
||||
if (!playerCharacter.is_alive()) {
|
||||
m_world.query<EntityNameComponent>().each(
|
||||
[&](flecs::entity ec, EntityNameComponent &en) {
|
||||
if (!playerCharacter.is_alive() &&
|
||||
en.name == pc.targetCharacterName)
|
||||
playerCharacter = ec;
|
||||
});
|
||||
actuatorDistance = pc.actuatorDistance;
|
||||
actuatorCooldown = pc.actuatorCooldown;
|
||||
m_circleColor = ImVec4(pc.actuatorColor.x,
|
||||
pc.actuatorColor.y,
|
||||
pc.actuatorColor.z, 1.0f);
|
||||
m_distantRadius = pc.distantCircleRadius;
|
||||
m_nearRadius = pc.nearCircleRadius;
|
||||
m_labelFontSize = pc.actuatorLabelFontSize;
|
||||
}
|
||||
});
|
||||
|
||||
if (!playerCharacter.is_alive() ||
|
||||
!playerCharacter.has<TransformComponent>())
|
||||
return;
|
||||
|
||||
Ogre::Vector3 charPos =
|
||||
playerCharacter.get<TransformComponent>().node
|
||||
->_getDerivedPosition();
|
||||
Ogre::Vector3 charPos = playerCharacter.get<TransformComponent>()
|
||||
.node->_getDerivedPosition();
|
||||
|
||||
// Collect actuators within distance
|
||||
m_visibleActuators.clear();
|
||||
std::vector<ScreenActuator> inRangeActuators;
|
||||
|
||||
m_world.query<ActuatorComponent, TransformComponent>()
|
||||
.each([&](flecs::entity e, ActuatorComponent &actuator,
|
||||
TransformComponent &trans) {
|
||||
// --- Collect ActuatorComponent entities ---
|
||||
m_world.query<ActuatorComponent, TransformComponent>().each(
|
||||
[&](flecs::entity e, ActuatorComponent &actuator,
|
||||
TransformComponent &trans) {
|
||||
// Skip if on cooldown or executing
|
||||
if (actuator.cooldownTimer > 0.0f || actuator.isExecuting)
|
||||
if (actuator.cooldownTimer > 0.0f ||
|
||||
actuator.isExecuting)
|
||||
return;
|
||||
|
||||
if (!trans.node)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 objPos = trans.node->_getDerivedPosition();
|
||||
Ogre::Vector3 objPos =
|
||||
trans.node->_getDerivedPosition();
|
||||
float dist = charPos.distance(objPos);
|
||||
if (dist > actuatorDistance)
|
||||
return;
|
||||
@@ -313,6 +321,44 @@ void ActuatorSystem::update(float deltaTime)
|
||||
}
|
||||
});
|
||||
|
||||
// --- Collect ItemComponent entities (no ActuatorComponent) ---
|
||||
m_world.query<ItemComponent, TransformComponent>().each(
|
||||
[&](flecs::entity e, ItemComponent &item,
|
||||
TransformComponent &trans) {
|
||||
// Skip items that also have an ActuatorComponent
|
||||
// (those are handled above as actuators)
|
||||
if (e.has<ActuatorComponent>())
|
||||
return;
|
||||
|
||||
if (!trans.node)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 objPos =
|
||||
trans.node->_getDerivedPosition();
|
||||
float dist = charPos.distance(objPos);
|
||||
if (dist > actuatorDistance)
|
||||
return;
|
||||
|
||||
Ogre::Vector2 screenPos = projectToScreen(objPos);
|
||||
if (screenPos.x < 0)
|
||||
return;
|
||||
|
||||
ScreenActuator sa;
|
||||
sa.entity = e;
|
||||
sa.screenPos = screenPos;
|
||||
sa.distance = dist;
|
||||
ImVec2 vpSize = ImGui::GetMainViewport()->Size;
|
||||
sa.distToScreenCenter =
|
||||
std::abs(screenPos.x - vpSize.x * 0.5f);
|
||||
|
||||
m_visibleActuators.push_back(sa);
|
||||
|
||||
// Items use a default pickup range (same as actuator defaults)
|
||||
if (isInRange(charPos, objPos, 1.5f, 1.8f)) {
|
||||
inRangeActuators.push_back(sa);
|
||||
}
|
||||
});
|
||||
|
||||
// Determine target actuator for interaction
|
||||
m_targetIndex = -1;
|
||||
if (!inRangeActuators.empty()) {
|
||||
@@ -341,14 +387,23 @@ void ActuatorSystem::update(float deltaTime)
|
||||
// Build label text
|
||||
m_labelText.clear();
|
||||
if (m_targetIndex >= 0) {
|
||||
flecs::entity targetEntity = m_visibleActuators[m_targetIndex].entity;
|
||||
if (targetEntity.is_alive() && targetEntity.has<ActuatorComponent>()) {
|
||||
auto &actuator = targetEntity.get<ActuatorComponent>();
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
m_labelText = "E " + actuator.actionNames[0];
|
||||
} else if (actuator.actionNames.size() > 1) {
|
||||
m_labelText = "E";
|
||||
flecs::entity targetEntity =
|
||||
m_visibleActuators[m_targetIndex].entity;
|
||||
if (targetEntity.is_alive()) {
|
||||
if (targetEntity.has<ActuatorComponent>()) {
|
||||
auto &actuator =
|
||||
targetEntity.get<ActuatorComponent>();
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
m_labelText =
|
||||
"E " + actuator.actionNames[0];
|
||||
} else if (actuator.actionNames.size() > 1) {
|
||||
m_labelText = "E";
|
||||
}
|
||||
} else if (targetEntity.has<ItemComponent>()) {
|
||||
// Show "E - Pick up [ItemName]" for items
|
||||
auto &item = targetEntity.get<ItemComponent>();
|
||||
m_labelText = "E Pick up " + item.itemName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,24 +443,37 @@ void ActuatorSystem::update(float deltaTime)
|
||||
if (m_targetIndex >= 0 && input.e) {
|
||||
flecs::entity targetEntity =
|
||||
m_visibleActuators[m_targetIndex].entity;
|
||||
auto &actuator = targetEntity.get<ActuatorComponent>();
|
||||
m_eHoldTime += deltaTime;
|
||||
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
if (input.ePressed) {
|
||||
executeAction(playerCharacter, targetEntity,
|
||||
actuator.actionNames[0]);
|
||||
m_eHoldTime = 0.0f;
|
||||
if (targetEntity.has<ActuatorComponent>()) {
|
||||
auto &actuator = targetEntity.get<ActuatorComponent>();
|
||||
m_eHoldTime += deltaTime;
|
||||
|
||||
if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
if (input.ePressed) {
|
||||
executeAction(playerCharacter,
|
||||
targetEntity,
|
||||
actuator.actionNames[0]);
|
||||
m_eHoldTime = 0.0f;
|
||||
}
|
||||
} else if (actuator.actionNames.size() > 1) {
|
||||
if (m_eHoldTime > 0.3f && !m_menuOpen) {
|
||||
m_menuOpen = true;
|
||||
m_menuActuatorId = targetEntity.id();
|
||||
m_eWasHeld = true;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(
|
||||
false);
|
||||
}
|
||||
}
|
||||
} else if (actuator.actionNames.size() > 1) {
|
||||
if (m_eHoldTime > 0.3f && !m_menuOpen) {
|
||||
m_menuOpen = true;
|
||||
m_menuActuatorId = targetEntity.id();
|
||||
m_eWasHeld = true;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(false);
|
||||
} else if (targetEntity.has<ItemComponent>() &&
|
||||
input.ePressed) {
|
||||
// Pick up item
|
||||
if (m_itemSystem) {
|
||||
m_itemSystem->pickupItem(playerCharacter,
|
||||
targetEntity);
|
||||
}
|
||||
m_eHoldTime = 0.0f;
|
||||
}
|
||||
} else {
|
||||
m_eHoldTime = 0.0f;
|
||||
@@ -427,11 +495,10 @@ void ActuatorSystem::render()
|
||||
return;
|
||||
|
||||
ImColor defaultColor(m_circleColor);
|
||||
ImColor targetColor(
|
||||
ImVec4(std::min(m_circleColor.x + 0.2f, 1.0f),
|
||||
std::min(m_circleColor.y + 0.3f, 1.0f),
|
||||
std::min(m_circleColor.z + 0.0f, 1.0f),
|
||||
1.0f));
|
||||
ImColor targetColor(ImVec4(std::min(m_circleColor.x + 0.2f, 1.0f),
|
||||
std::min(m_circleColor.y + 0.3f, 1.0f),
|
||||
std::min(m_circleColor.z + 0.0f, 1.0f),
|
||||
1.0f));
|
||||
|
||||
for (size_t i = 0; i < m_visibleActuators.size(); i++) {
|
||||
bool isTarget = (static_cast<int>(i) == m_targetIndex);
|
||||
@@ -453,8 +520,7 @@ void ActuatorSystem::render()
|
||||
|
||||
ImFont *font = ImGui::GetFont();
|
||||
ImVec2 textSize = font->CalcTextSizeA(
|
||||
m_labelFontSize, FLT_MAX, -1.0f,
|
||||
m_labelText.c_str());
|
||||
m_labelFontSize, FLT_MAX, -1.0f, m_labelText.c_str());
|
||||
|
||||
ImVec2 textPos(center.x - (textSize.x * 0.5f),
|
||||
center.y + circleRadius + 6.0f);
|
||||
|
||||
@@ -10,18 +10,25 @@
|
||||
|
||||
class EditorApp;
|
||||
class BehaviorTreeSystem;
|
||||
class ItemSystem;
|
||||
|
||||
/**
|
||||
* System that handles player interaction with Actuator entities.
|
||||
* System that handles player interaction with Actuator entities
|
||||
* and Item entities.
|
||||
*
|
||||
* In game mode:
|
||||
* - update() finds nearby actuators and handles input
|
||||
* - update() finds nearby actuators and items, handles input
|
||||
* - render() draws on-screen circle markers (called in preViewportUpdate after NewFrame)
|
||||
* - Shows "E - ActionName" (or just "E" for multi-action) when in reach
|
||||
* - Shows "E - Pick up [ItemName]" for items
|
||||
* - Handles E press / hold for action activation
|
||||
* - Manages per-actuator cooldowns
|
||||
* - Executes actions via BehaviorTreeSystem directly (no ActionDebug)
|
||||
* - Disables player controls during action execution
|
||||
*
|
||||
* Items (entities with ItemComponent but no ActuatorComponent) are
|
||||
* detected automatically and shown with a "Pick up" prompt. On E press,
|
||||
* the item is added to the player's inventory via ItemSystem.
|
||||
*/
|
||||
class ActuatorSystem {
|
||||
public:
|
||||
@@ -29,6 +36,15 @@ public:
|
||||
EditorApp *editorApp, BehaviorTreeSystem *btSystem);
|
||||
~ActuatorSystem();
|
||||
|
||||
/**
|
||||
* Set the ItemSystem for item pickup handling.
|
||||
* Must be called after construction.
|
||||
*/
|
||||
void setItemSystem(ItemSystem *system)
|
||||
{
|
||||
m_itemSystem = system;
|
||||
}
|
||||
|
||||
void update(float deltaTime);
|
||||
void render();
|
||||
|
||||
@@ -43,7 +59,8 @@ private:
|
||||
Ogre::Vector2 projectToScreen(const Ogre::Vector3 &worldPoint);
|
||||
bool isInRange(const Ogre::Vector3 &charPos,
|
||||
const Ogre::Vector3 &objPos, float radius, float height);
|
||||
void executeAction(flecs::entity character, flecs::entity actuatorEntity,
|
||||
void executeAction(flecs::entity character,
|
||||
flecs::entity actuatorEntity,
|
||||
const Ogre::String &actionName);
|
||||
bool isActionComplete(flecs::entity character, float deltaTime);
|
||||
void drawActionMenu(flecs::entity actuatorEntity);
|
||||
@@ -53,6 +70,7 @@ private:
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
EditorApp *m_editorApp;
|
||||
BehaviorTreeSystem *m_btSystem;
|
||||
ItemSystem *m_itemSystem = nullptr;
|
||||
|
||||
// Cached data between update() and render()
|
||||
std::vector<ScreenActuator> m_visibleActuators;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "AnimationTreeSystem.hpp"
|
||||
#include "CharacterSystem.hpp"
|
||||
#include "SmartObjectSystem.hpp"
|
||||
#include "ItemSystem.hpp"
|
||||
#include "EventBus.hpp"
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
@@ -10,6 +11,8 @@
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <iostream>
|
||||
#include <cstdlib>
|
||||
@@ -18,7 +21,8 @@
|
||||
static float g_epsilon = 0.0001f;
|
||||
|
||||
static bool parseValueString(const Ogre::String &str, int &outInt,
|
||||
float &outFloat, Ogre::Vector3 &outVec3, int &type);
|
||||
float &outFloat, Ogre::Vector3 &outVec3,
|
||||
int &type);
|
||||
|
||||
/** Parse "key=val,key2=val2" params into a GoapBlackboard.
|
||||
* Values are auto-detected: int, float, vec3 (x,y,z), or quoted string. */
|
||||
@@ -574,6 +578,267 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
/* --- Item / Inventory nodes --- */
|
||||
/* These nodes need an ItemSystem instance. We look it up via
|
||||
* the EditorApp singleton pattern (stored in SmartObjectSystem). */
|
||||
ItemSystem *itemSystem = nullptr;
|
||||
{
|
||||
SmartObjectSystem *soSystem = SmartObjectSystem::getInstance();
|
||||
if (soSystem)
|
||||
itemSystem = soSystem->getItemSystem();
|
||||
}
|
||||
|
||||
if (node.type == "hasItem") {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
return itemSystem->hasItem(e, node.name) ? Status::success :
|
||||
Status::failure;
|
||||
}
|
||||
|
||||
if (node.type == "hasItemByName") {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
return itemSystem->hasItemByName(e, node.name) ?
|
||||
Status::success :
|
||||
Status::failure;
|
||||
}
|
||||
|
||||
if (node.type == "countItem") {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
int required = 1;
|
||||
if (!node.params.empty()) {
|
||||
char *end = nullptr;
|
||||
long val = strtol(node.params.c_str(), &end, 10);
|
||||
if (end != node.params.c_str() && *end == '\0' &&
|
||||
val > 0)
|
||||
required = static_cast<int>(val);
|
||||
}
|
||||
int count = itemSystem->countItem(e, node.name);
|
||||
return count >= required ? Status::success : Status::failure;
|
||||
}
|
||||
|
||||
if (node.type == "pickupItem") {
|
||||
if (isNewlyActive(state, &node)) {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
|
||||
// Find nearest ItemComponent entity matching the
|
||||
// optional itemId filter (node.name)
|
||||
flecs::entity nearestItem = flecs::entity::null();
|
||||
float nearestDist = std::numeric_limits<float>::max();
|
||||
|
||||
Ogre::Vector3 charPos = Ogre::Vector3::ZERO;
|
||||
if (e.has<TransformComponent>()) {
|
||||
auto &trans = e.get<TransformComponent>();
|
||||
if (trans.node)
|
||||
charPos =
|
||||
trans.node
|
||||
->_getDerivedPosition();
|
||||
else
|
||||
charPos = trans.position;
|
||||
}
|
||||
|
||||
m_world.query<ItemComponent, TransformComponent>().each(
|
||||
[&](flecs::entity itemEntity,
|
||||
ItemComponent &itemComp,
|
||||
TransformComponent &itemTrans) {
|
||||
// Skip items that are in containers
|
||||
// (they have no TransformComponent in world space)
|
||||
if (!itemTrans.node &&
|
||||
itemTrans.position ==
|
||||
Ogre::Vector3::ZERO)
|
||||
return;
|
||||
if (!node.name.empty() &&
|
||||
itemComp.itemId != node.name)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 itemPos =
|
||||
Ogre::Vector3::ZERO;
|
||||
if (itemTrans.node)
|
||||
itemPos =
|
||||
itemTrans.node
|
||||
->_getDerivedPosition();
|
||||
else
|
||||
itemPos = itemTrans.position;
|
||||
|
||||
float dist = charPos.distance(itemPos);
|
||||
// Default pickup radius of 1.5 units
|
||||
if (dist < nearestDist && dist < 1.5f) {
|
||||
nearestDist = dist;
|
||||
nearestItem = itemEntity;
|
||||
}
|
||||
});
|
||||
|
||||
if (nearestItem.is_alive()) {
|
||||
itemSystem->addItemEntityToInventory(
|
||||
e, nearestItem);
|
||||
std::cout << "[BT] pickupItem: picked up "
|
||||
<< nearestItem.id() << std::endl;
|
||||
return Status::success;
|
||||
}
|
||||
return Status::failure;
|
||||
}
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
if (node.type == "dropItem") {
|
||||
if (isNewlyActive(state, &node)) {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
|
||||
int count = 1;
|
||||
if (!node.params.empty()) {
|
||||
char *end = nullptr;
|
||||
long val =
|
||||
strtol(node.params.c_str(), &end, 10);
|
||||
if (end != node.params.c_str() &&
|
||||
*end == '\0' && val > 0)
|
||||
count = static_cast<int>(val);
|
||||
}
|
||||
|
||||
// Find the item in inventory
|
||||
int slotIdx = -1;
|
||||
auto &inv = e.get<InventoryComponent>();
|
||||
for (int i = 0; i < (int)inv.slots.size(); i++) {
|
||||
if (!inv.slots[i].isEmpty() &&
|
||||
inv.slots[i].itemId == node.name) {
|
||||
slotIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (slotIdx < 0)
|
||||
return Status::failure;
|
||||
|
||||
// Get drop position (in front of character)
|
||||
Ogre::Vector3 dropPos = Ogre::Vector3::ZERO;
|
||||
if (e.has<TransformComponent>()) {
|
||||
auto &trans = e.get<TransformComponent>();
|
||||
if (trans.node) {
|
||||
dropPos =
|
||||
trans.node
|
||||
->_getDerivedPosition();
|
||||
Ogre::Vector3 forward =
|
||||
trans.node
|
||||
->_getDerivedOrientation() *
|
||||
Ogre::Vector3(0, 0, -1);
|
||||
forward.y = 0;
|
||||
forward.normalise();
|
||||
dropPos += forward * 1.5f;
|
||||
} else {
|
||||
dropPos = trans.position;
|
||||
}
|
||||
}
|
||||
|
||||
itemSystem->dropItem(e, slotIdx, dropPos, count);
|
||||
std::cout << "[BT] dropItem: dropped " << node.name
|
||||
<< " x" << count << std::endl;
|
||||
return Status::success;
|
||||
}
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
if (node.type == "useItem") {
|
||||
if (isNewlyActive(state, &node)) {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
|
||||
// Find the item in inventory
|
||||
int slotIdx = -1;
|
||||
auto &inv = e.get<InventoryComponent>();
|
||||
for (int i = 0; i < (int)inv.slots.size(); i++) {
|
||||
if (!inv.slots[i].isEmpty() &&
|
||||
inv.slots[i].itemId == node.name) {
|
||||
slotIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (slotIdx < 0)
|
||||
return Status::failure;
|
||||
|
||||
itemSystem->useItem(e, e, slotIdx);
|
||||
std::cout << "[BT] useItem: used " << node.name
|
||||
<< std::endl;
|
||||
return Status::success;
|
||||
}
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
if (node.type == "addItemToInventory") {
|
||||
if (isNewlyActive(state, &node)) {
|
||||
if (!itemSystem || !e.has<InventoryComponent>())
|
||||
return Status::failure;
|
||||
|
||||
// Parse params: "itemId,itemName,itemType,count,weight,value"
|
||||
Ogre::String itemId = node.name;
|
||||
Ogre::String itemName = node.name;
|
||||
Ogre::String itemType = "misc";
|
||||
int count = 1;
|
||||
float weight = 0.1f;
|
||||
int value = 1;
|
||||
|
||||
if (!node.params.empty()) {
|
||||
// Parse comma-separated values
|
||||
std::vector<Ogre::String> parts;
|
||||
const char *s = node.params.c_str();
|
||||
const char *start = s;
|
||||
while (*s) {
|
||||
if (*s == ',') {
|
||||
parts.push_back(Ogre::String(
|
||||
start,
|
||||
static_cast<size_t>(
|
||||
s - start)));
|
||||
start = s + 1;
|
||||
}
|
||||
s++;
|
||||
}
|
||||
if (s > start)
|
||||
parts.push_back(Ogre::String(
|
||||
start, static_cast<size_t>(
|
||||
s - start)));
|
||||
|
||||
if (parts.size() >= 1)
|
||||
itemName = parts[0];
|
||||
if (parts.size() >= 2)
|
||||
itemType = parts[1];
|
||||
if (parts.size() >= 3) {
|
||||
char *end = nullptr;
|
||||
long val = strtol(parts[2].c_str(),
|
||||
&end, 10);
|
||||
if (end != parts[2].c_str() &&
|
||||
*end == '\0' && val > 0)
|
||||
count = static_cast<int>(val);
|
||||
}
|
||||
if (parts.size() >= 4) {
|
||||
char *end = nullptr;
|
||||
float val =
|
||||
strtof(parts[3].c_str(), &end);
|
||||
if (end != parts[3].c_str() &&
|
||||
*end == '\0' && val >= 0.0f)
|
||||
weight = val;
|
||||
}
|
||||
if (parts.size() >= 5) {
|
||||
char *end = nullptr;
|
||||
long val = strtol(parts[4].c_str(),
|
||||
&end, 10);
|
||||
if (end != parts[4].c_str() &&
|
||||
*end == '\0' && val >= 0)
|
||||
value = static_cast<int>(val);
|
||||
}
|
||||
}
|
||||
|
||||
itemSystem->addItemToInventory(e, itemId, itemName,
|
||||
itemType, count, weight,
|
||||
value);
|
||||
std::cout << "[BT] addItemToInventory: added "
|
||||
<< itemName << " x" << count << std::endl;
|
||||
return Status::success;
|
||||
}
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
/* --- Teleport to Smart Object child node --- */
|
||||
if (node.type == "teleportToChild") {
|
||||
if (isNewlyActive(state, &node)) {
|
||||
@@ -782,8 +1047,8 @@ BehaviorTreeSystem::evaluatePlayerAction(flecs::entity_t id,
|
||||
state.treeResult = Status::running;
|
||||
}
|
||||
state.currentActiveLeaves.clear();
|
||||
Status result = evaluateNode(root, m_world.entity(id), state,
|
||||
deltaTime);
|
||||
Status result =
|
||||
evaluateNode(root, m_world.entity(id), state, deltaTime);
|
||||
state.lastActiveLeaves = state.currentActiveLeaves;
|
||||
state.firstRun = false;
|
||||
state.treeResult = result;
|
||||
|
||||
224
src/features/editScene/systems/DialogueSystem.cpp
Normal file
224
src/features/editScene/systems/DialogueSystem.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
#include "DialogueSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "../components/DialogueComponent.hpp"
|
||||
#include "../systems/EventBus.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include <imgui.h>
|
||||
#include <OgreFontManager.h>
|
||||
#include <OgreImGuiOverlay.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreOverlayManager.h>
|
||||
|
||||
DialogueSystem::DialogueSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_editorApp(editorApp)
|
||||
{
|
||||
// Subscribe to dialogue events
|
||||
EventBus::getInstance().subscribe(
|
||||
"dialogue_show",
|
||||
[this](const Ogre::String &, const GoapBlackboard ¶ms) {
|
||||
// Find the first entity with DialogueComponent
|
||||
m_world.query<DialogueComponent>().each([&](flecs::entity
|
||||
e,
|
||||
DialogueComponent
|
||||
&dc) {
|
||||
if (!dc.enabled)
|
||||
return;
|
||||
|
||||
Ogre::String text =
|
||||
params.getStringValue("text");
|
||||
if (text.empty())
|
||||
return;
|
||||
|
||||
// Parse choices from comma-separated
|
||||
// string
|
||||
std::vector<Ogre::String> choices;
|
||||
Ogre::String choicesStr =
|
||||
params.getStringValue("choices");
|
||||
if (!choicesStr.empty()) {
|
||||
Ogre::String::size_type start = 0;
|
||||
Ogre::String::size_type end;
|
||||
while ((end = choicesStr.find(",",
|
||||
start)) !=
|
||||
Ogre::String::npos) {
|
||||
choices.push_back(
|
||||
choicesStr.substr(
|
||||
start,
|
||||
end - start));
|
||||
start = end + 1;
|
||||
}
|
||||
if (start < choicesStr.length())
|
||||
choices.push_back(
|
||||
choicesStr.substr(
|
||||
start));
|
||||
}
|
||||
|
||||
Ogre::String speaker =
|
||||
params.getStringValue("speaker");
|
||||
|
||||
dc.show(text, choices, speaker);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
DialogueSystem::~DialogueSystem()
|
||||
{
|
||||
// EventBus subscriptions are managed externally
|
||||
}
|
||||
|
||||
void DialogueSystem::ensureFontLoaded(const Ogre::String &fontName,
|
||||
float fontSize)
|
||||
{
|
||||
if (m_fontLoaded && m_currentFontName == fontName &&
|
||||
m_currentFontSize == fontSize)
|
||||
return;
|
||||
|
||||
Ogre::ImGuiOverlay *overlay = m_editorApp->getImGuiOverlay();
|
||||
if (!overlay)
|
||||
return;
|
||||
|
||||
// Load the main dialogue font
|
||||
Ogre::FontPtr font;
|
||||
try {
|
||||
if (Ogre::FontManager::getSingleton().resourceExists(
|
||||
"DialogueFont", "General")) {
|
||||
Ogre::FontManager::getSingleton().remove("DialogueFont",
|
||||
"General");
|
||||
}
|
||||
font = Ogre::FontManager::getSingleton().create("DialogueFont",
|
||||
"General");
|
||||
font->setType(Ogre::FontType::FT_TRUETYPE);
|
||||
font->setSource(fontName);
|
||||
font->setTrueTypeSize(fontSize);
|
||||
font->setTrueTypeResolution(75);
|
||||
font->addCodePointRange(Ogre::Font::CodePointRange(32, 255));
|
||||
font->load();
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"DialogueSystem: Failed to load font " + fontName);
|
||||
m_dialogueFont = nullptr;
|
||||
m_fontLoaded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_dialogueFont = overlay->addFont("DialogueFont", "General");
|
||||
m_currentFontName = fontName;
|
||||
m_currentFontSize = fontSize;
|
||||
m_fontLoaded = true;
|
||||
}
|
||||
|
||||
void DialogueSystem::prepareFont()
|
||||
{
|
||||
if (!m_editorApp)
|
||||
return;
|
||||
|
||||
// Find an entity with DialogueComponent
|
||||
flecs::entity dialogueEntity = flecs::entity::null();
|
||||
m_world.query<DialogueComponent>().each(
|
||||
[&](flecs::entity e, DialogueComponent &) {
|
||||
if (!dialogueEntity.is_alive())
|
||||
dialogueEntity = e;
|
||||
});
|
||||
|
||||
if (dialogueEntity.is_alive()) {
|
||||
auto &dc = dialogueEntity.get_mut<DialogueComponent>();
|
||||
ensureFontLoaded(dc.fontName, dc.fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
void DialogueSystem::update(float deltaTime)
|
||||
{
|
||||
(void)deltaTime;
|
||||
|
||||
if (!m_editorApp ||
|
||||
m_editorApp->getGameMode() != EditorApp::GameMode::Game ||
|
||||
m_editorApp->getGamePlayState() !=
|
||||
EditorApp::GamePlayState::Playing)
|
||||
return;
|
||||
|
||||
// Find an entity with DialogueComponent
|
||||
flecs::entity dialogueEntity = flecs::entity::null();
|
||||
m_world.query<DialogueComponent>().each(
|
||||
[&](flecs::entity e, DialogueComponent &) {
|
||||
if (!dialogueEntity.is_alive())
|
||||
dialogueEntity = e;
|
||||
});
|
||||
|
||||
if (!dialogueEntity.is_alive())
|
||||
return;
|
||||
|
||||
auto &dc = dialogueEntity.get_mut<DialogueComponent>();
|
||||
if (!dc.enabled || !dc.isActive())
|
||||
return;
|
||||
|
||||
renderDialogueBox(dc);
|
||||
}
|
||||
|
||||
void DialogueSystem::renderDialogueBox(DialogueComponent &dc)
|
||||
{
|
||||
ImVec2 size = ImGui::GetMainViewport()->Size;
|
||||
|
||||
float boxHeight = size.y * dc.boxHeightFraction;
|
||||
float boxY = size.y * dc.boxPositionFraction;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(0, boxY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(size.x, boxHeight), ImGuiCond_Always);
|
||||
|
||||
// Semi-transparent background
|
||||
ImVec4 bgColor = ImVec4(0.0f, 0.0f, 0.0f, dc.backgroundOpacity);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, bgColor);
|
||||
|
||||
ImGui::Begin(
|
||||
"DialogueBox", nullptr,
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing);
|
||||
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
|
||||
// Speaker name (if provided)
|
||||
if (!dc.speaker.empty()) {
|
||||
if (m_speakerFont)
|
||||
ImGui::PushFont(m_speakerFont);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "%s",
|
||||
dc.speaker.c_str());
|
||||
if (m_speakerFont)
|
||||
ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Narration text
|
||||
if (m_dialogueFont)
|
||||
ImGui::PushFont(m_dialogueFont);
|
||||
|
||||
ImGui::TextWrapped("%s", dc.text.c_str());
|
||||
|
||||
if (m_dialogueFont)
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Choices or click-to-progress
|
||||
if (dc.choices.empty()) {
|
||||
// No choices: click anywhere to progress
|
||||
ImGui::SetCursorScreenPos(p);
|
||||
if (ImGui::InvisibleButton("DialogueProgress",
|
||||
ImGui::GetWindowSize())) {
|
||||
dc.progress();
|
||||
}
|
||||
} else {
|
||||
// Choices: render as buttons
|
||||
for (int i = 0; i < (int)dc.choices.size(); i++) {
|
||||
if (ImGui::Button(dc.choices[i].c_str())) {
|
||||
dc.selectChoice(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
58
src/features/editScene/systems/DialogueSystem.hpp
Normal file
58
src/features/editScene/systems/DialogueSystem.hpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#ifndef EDITSCENE_DIALOGUESYSTEM_HPP
|
||||
#define EDITSCENE_DIALOGUESYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <imgui.h>
|
||||
#include <memory>
|
||||
|
||||
#include "../components/DialogueComponent.hpp"
|
||||
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
* System that renders the visual-novel style dialogue box in game mode.
|
||||
*
|
||||
* Only active when EditorApp is in GameMode::Game and
|
||||
* GamePlayState::Playing. The dialogue box is rendered at the bottom
|
||||
* of the screen, showing narration text and optional player choices.
|
||||
*
|
||||
* The dialogue can be triggered via:
|
||||
* 1. EventBus event "dialogue_show" with GoapBlackboard payload
|
||||
* 2. Direct API on DialogueComponent
|
||||
*/
|
||||
class DialogueSystem {
|
||||
public:
|
||||
DialogueSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp);
|
||||
~DialogueSystem();
|
||||
|
||||
/**
|
||||
* Update and render the dialogue box.
|
||||
* Must be called inside an active ImGui frame.
|
||||
*/
|
||||
void update(float deltaTime);
|
||||
|
||||
/**
|
||||
* Pre-load the dialogue font before ImGui NewFrame().
|
||||
* Must be called outside an active ImGui frame (before NewFrame).
|
||||
*/
|
||||
void prepareFont();
|
||||
|
||||
private:
|
||||
void renderDialogueBox(DialogueComponent &dc);
|
||||
void ensureFontLoaded(const Ogre::String &fontName, float fontSize);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
EditorApp *m_editorApp;
|
||||
|
||||
bool m_fontLoaded = false;
|
||||
Ogre::String m_currentFontName;
|
||||
float m_currentFontSize = 0.0f;
|
||||
ImFont *m_dialogueFont = nullptr;
|
||||
ImFont *m_speakerFont = nullptr;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DIALOGUESYSTEM_HPP
|
||||
@@ -42,6 +42,8 @@
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/EventHandler.hpp"
|
||||
#include "../components/PrefabInstance.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
|
||||
#include "../ui/TransformEditor.hpp"
|
||||
#include "../ui/RenderableEditor.hpp"
|
||||
@@ -1080,6 +1082,20 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render Item if present
|
||||
if (entity.has<ItemComponent>()) {
|
||||
auto &item = entity.get_mut<ItemComponent>();
|
||||
m_componentRegistry.render<ItemComponent>(entity, item);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render Inventory if present
|
||||
if (entity.has<InventoryComponent>()) {
|
||||
auto &inv = entity.get_mut<InventoryComponent>();
|
||||
m_componentRegistry.render<InventoryComponent>(entity, inv);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Show message if no components
|
||||
|
||||
if (componentCount == 0) {
|
||||
|
||||
364
src/features/editScene/systems/ItemSystem.cpp
Normal file
364
src/features/editScene/systems/ItemSystem.cpp
Normal file
@@ -0,0 +1,364 @@
|
||||
#include "ItemSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "BehaviorTreeSystem.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <cmath>
|
||||
|
||||
ItemSystem::ItemSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp, BehaviorTreeSystem *btSystem)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_editorApp(editorApp)
|
||||
, m_btSystem(btSystem)
|
||||
{
|
||||
}
|
||||
|
||||
ItemSystem::~ItemSystem() = default;
|
||||
|
||||
// --- Inventory manipulation API ---
|
||||
|
||||
bool ItemSystem::addItemToInventory(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId,
|
||||
const Ogre::String &itemName,
|
||||
const Ogre::String &itemType, int stackSize,
|
||||
float weight, int value,
|
||||
const Ogre::String &useActionName)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
auto &inv = inventoryEntity.get_mut<InventoryComponent>();
|
||||
|
||||
// Try to stack with existing items of the same itemId
|
||||
if (!itemId.empty()) {
|
||||
for (int i = 0; i < (int)inv.slots.size(); i++) {
|
||||
auto &slot = inv.slots[i];
|
||||
if (!slot.isEmpty() && slot.itemId == itemId &&
|
||||
slot.stackSize < slot.maxStackSize) {
|
||||
int space = slot.maxStackSize - slot.stackSize;
|
||||
int add = std::min(space, stackSize);
|
||||
slot.stackSize += add;
|
||||
stackSize -= add;
|
||||
if (stackSize <= 0) {
|
||||
inv.recalculateWeight();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find empty slot for remaining items
|
||||
while (stackSize > 0) {
|
||||
int slotIdx = inv.findEmptySlot();
|
||||
if (slotIdx < 0)
|
||||
return false; // Inventory full
|
||||
|
||||
// Ensure slots vector is large enough
|
||||
while ((int)inv.slots.size() <= slotIdx)
|
||||
inv.slots.emplace_back();
|
||||
|
||||
auto &slot = inv.slots[slotIdx];
|
||||
slot.itemEntity = 0;
|
||||
slot.itemId = itemId;
|
||||
slot.itemName = itemName;
|
||||
slot.itemType = itemType;
|
||||
slot.maxStackSize = 99;
|
||||
slot.weight = weight;
|
||||
slot.value = value;
|
||||
slot.useActionName = useActionName;
|
||||
|
||||
int add = std::min(stackSize, slot.maxStackSize);
|
||||
slot.stackSize = add;
|
||||
stackSize -= add;
|
||||
}
|
||||
|
||||
inv.recalculateWeight();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ItemSystem::addItemEntityToInventory(flecs::entity inventoryEntity,
|
||||
flecs::entity itemEntity)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
if (!itemEntity.is_alive() || !itemEntity.has<ItemComponent>())
|
||||
return false;
|
||||
|
||||
auto &item = itemEntity.get_mut<ItemComponent>();
|
||||
|
||||
// Add to inventory
|
||||
bool result = addItemToInventory(inventoryEntity, item.itemId,
|
||||
item.itemName, item.itemType,
|
||||
item.stackSize, item.weight,
|
||||
item.value, item.useActionName);
|
||||
|
||||
if (result) {
|
||||
// Hide the world entity
|
||||
if (itemEntity.has<TransformComponent>()) {
|
||||
auto &trans = itemEntity.get_mut<TransformComponent>();
|
||||
if (trans.node)
|
||||
trans.node->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int ItemSystem::removeItemFromInventory(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId, int count)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return 0;
|
||||
|
||||
auto &inv = inventoryEntity.get_mut<InventoryComponent>();
|
||||
int removed = 0;
|
||||
|
||||
for (int i = (int)inv.slots.size() - 1; i >= 0 && count > 0; i--) {
|
||||
auto &slot = inv.slots[i];
|
||||
if (slot.isEmpty() || slot.itemId != itemId)
|
||||
continue;
|
||||
|
||||
int remove = std::min(count, slot.stackSize);
|
||||
slot.stackSize -= remove;
|
||||
count -= remove;
|
||||
removed += remove;
|
||||
|
||||
if (slot.stackSize <= 0)
|
||||
slot.clear();
|
||||
}
|
||||
|
||||
if (removed > 0)
|
||||
inv.recalculateWeight();
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
bool ItemSystem::removeItemFromSlot(flecs::entity inventoryEntity,
|
||||
int slotIndex, int count)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
auto &inv = inventoryEntity.get_mut<InventoryComponent>();
|
||||
if (slotIndex < 0 || slotIndex >= (int)inv.slots.size())
|
||||
return false;
|
||||
|
||||
auto &slot = inv.slots[slotIndex];
|
||||
if (slot.isEmpty())
|
||||
return false;
|
||||
|
||||
int remove = std::min(count, slot.stackSize);
|
||||
slot.stackSize -= remove;
|
||||
|
||||
if (slot.stackSize <= 0)
|
||||
slot.clear();
|
||||
|
||||
inv.recalculateWeight();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ItemSystem::transferItem(flecs::entity fromInventory, int slotIndex,
|
||||
flecs::entity toInventory, int count)
|
||||
{
|
||||
if (!fromInventory.is_alive() ||
|
||||
!fromInventory.has<InventoryComponent>())
|
||||
return false;
|
||||
if (!toInventory.is_alive() || !toInventory.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
auto &fromInv = fromInventory.get_mut<InventoryComponent>();
|
||||
if (slotIndex < 0 || slotIndex >= (int)fromInv.slots.size())
|
||||
return false;
|
||||
|
||||
auto &slot = fromInv.slots[slotIndex];
|
||||
if (slot.isEmpty())
|
||||
return false;
|
||||
|
||||
int transferCount = std::min(count, slot.stackSize);
|
||||
|
||||
// Add to target inventory
|
||||
bool added = addItemToInventory(toInventory, slot.itemId, slot.itemName,
|
||||
slot.itemType, transferCount,
|
||||
slot.weight, slot.value,
|
||||
slot.useActionName);
|
||||
|
||||
if (!added)
|
||||
return false;
|
||||
|
||||
// Remove from source
|
||||
slot.stackSize -= transferCount;
|
||||
if (slot.stackSize <= 0)
|
||||
slot.clear();
|
||||
|
||||
fromInv.recalculateWeight();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ItemSystem::hasItem(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId) const
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
return inventoryEntity.get<InventoryComponent>().hasItem(itemId);
|
||||
}
|
||||
|
||||
bool ItemSystem::hasItemByName(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemName) const
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
return inventoryEntity.get<InventoryComponent>().hasItemByName(
|
||||
itemName);
|
||||
}
|
||||
|
||||
int ItemSystem::countItem(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId) const
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return 0;
|
||||
|
||||
return inventoryEntity.get<InventoryComponent>().countItem(itemId);
|
||||
}
|
||||
|
||||
bool ItemSystem::dropItem(flecs::entity inventoryEntity, int slotIndex,
|
||||
const Ogre::Vector3 &worldPosition, int count)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
auto &inv = inventoryEntity.get_mut<InventoryComponent>();
|
||||
if (slotIndex < 0 || slotIndex >= (int)inv.slots.size())
|
||||
return false;
|
||||
|
||||
auto &slot = inv.slots[slotIndex];
|
||||
if (slot.isEmpty())
|
||||
return false;
|
||||
|
||||
int dropCount = std::min(count, slot.stackSize);
|
||||
|
||||
// If the item has a world entity reference, show it
|
||||
if (slot.itemEntity != 0) {
|
||||
flecs::entity itemEntity = m_world.entity(slot.itemEntity);
|
||||
if (itemEntity.is_alive() && itemEntity.has<ItemComponent>()) {
|
||||
auto &item = itemEntity.get_mut<ItemComponent>();
|
||||
item.stackSize = dropCount;
|
||||
|
||||
if (itemEntity.has<TransformComponent>()) {
|
||||
auto &trans =
|
||||
itemEntity.get_mut<TransformComponent>();
|
||||
trans.position = worldPosition;
|
||||
if (trans.node) {
|
||||
trans.node->setPosition(worldPosition);
|
||||
trans.node->setVisible(true);
|
||||
}
|
||||
trans.markChanged();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create a new world entity for the dropped item
|
||||
flecs::entity itemEntity = m_world.entity();
|
||||
itemEntity.set<ItemComponent>(
|
||||
ItemComponent(slot.itemName, slot.itemType));
|
||||
auto &item = itemEntity.get_mut<ItemComponent>();
|
||||
item.itemId = slot.itemId;
|
||||
item.stackSize = dropCount;
|
||||
item.weight = slot.weight;
|
||||
item.value = slot.value;
|
||||
item.useActionName = slot.useActionName;
|
||||
|
||||
// Create a scene node for the dropped item
|
||||
Ogre::SceneNode *node =
|
||||
m_sceneMgr->getRootSceneNode()->createChildSceneNode(
|
||||
worldPosition);
|
||||
TransformComponent trans;
|
||||
trans.node = node;
|
||||
trans.position = worldPosition;
|
||||
itemEntity.set<TransformComponent>(trans);
|
||||
|
||||
EntityNameComponent nameComp;
|
||||
nameComp.name = slot.itemName + "_dropped";
|
||||
itemEntity.set<EntityNameComponent>(nameComp);
|
||||
}
|
||||
|
||||
// Remove from inventory
|
||||
slot.stackSize -= dropCount;
|
||||
if (slot.stackSize <= 0)
|
||||
slot.clear();
|
||||
|
||||
inv.recalculateWeight();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ItemSystem::useItem(flecs::entity characterEntity,
|
||||
flecs::entity inventoryEntity, int slotIndex)
|
||||
{
|
||||
if (!inventoryEntity.is_alive() ||
|
||||
!inventoryEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
|
||||
auto &inv = inventoryEntity.get<InventoryComponent>();
|
||||
if (slotIndex < 0 || slotIndex >= (int)inv.slots.size())
|
||||
return false;
|
||||
|
||||
const auto &slot = inv.slots[slotIndex];
|
||||
if (slot.isEmpty() || slot.useActionName.empty())
|
||||
return false;
|
||||
|
||||
// Execute the use action via behavior tree
|
||||
if (m_btSystem && characterEntity.is_alive()) {
|
||||
// Look up the action in the database
|
||||
ActionDatabase *db = nullptr;
|
||||
m_world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
|
||||
if (db) {
|
||||
const GoapAction *action =
|
||||
db->findAction(slot.useActionName);
|
||||
if (action) {
|
||||
m_btSystem->evaluatePlayerAction(
|
||||
characterEntity.id(),
|
||||
action->behaviorTree, 0.016f, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ItemSystem::pickupItem(flecs::entity characterEntity,
|
||||
flecs::entity itemEntity)
|
||||
{
|
||||
if (!characterEntity.is_alive() ||
|
||||
!characterEntity.has<InventoryComponent>())
|
||||
return false;
|
||||
if (!itemEntity.is_alive() || !itemEntity.has<ItemComponent>())
|
||||
return false;
|
||||
|
||||
bool result = addItemEntityToInventory(characterEntity, itemEntity);
|
||||
if (result) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[ItemSystem] Picked up: " +
|
||||
itemEntity.get<ItemComponent>().itemName);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
88
src/features/editScene/systems/ItemSystem.hpp
Normal file
88
src/features/editScene/systems/ItemSystem.hpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#ifndef EDITSCENE_ITEM_SYSTEM_HPP
|
||||
#define EDITSCENE_ITEM_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
|
||||
class EditorApp;
|
||||
class BehaviorTreeSystem;
|
||||
|
||||
/**
|
||||
* System that handles item pickup, drop, use, and inventory management.
|
||||
*
|
||||
* Provides a pure API for inventory operations. Proximity detection
|
||||
* for player pickup is handled by ActuatorSystem (which detects
|
||||
* entities with ItemComponent and shows "E - Pick up" prompts).
|
||||
*
|
||||
* For AI characters, behavior tree nodes provide inventory access.
|
||||
*/
|
||||
class ItemSystem {
|
||||
public:
|
||||
ItemSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp, BehaviorTreeSystem *btSystem);
|
||||
~ItemSystem();
|
||||
|
||||
// --- Inventory manipulation API ---
|
||||
|
||||
/** Add an item to an inventory by itemId. Creates a new slot. */
|
||||
bool addItemToInventory(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId,
|
||||
const Ogre::String &itemName,
|
||||
const Ogre::String &itemType, int stackSize = 1,
|
||||
float weight = 0.1f, int value = 1,
|
||||
const Ogre::String &useActionName = "");
|
||||
|
||||
/** Add an item entity (ItemComponent) to an inventory. */
|
||||
bool addItemEntityToInventory(flecs::entity inventoryEntity,
|
||||
flecs::entity itemEntity);
|
||||
|
||||
/** Remove items from inventory by itemId. Returns number removed. */
|
||||
int removeItemFromInventory(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId, int count = 1);
|
||||
|
||||
/** Remove items from inventory by slot index. */
|
||||
bool removeItemFromSlot(flecs::entity inventoryEntity, int slotIndex,
|
||||
int count = 1);
|
||||
|
||||
/** Transfer items between two inventories. */
|
||||
bool transferItem(flecs::entity fromInventory, int slotIndex,
|
||||
flecs::entity toInventory, int count = 1);
|
||||
|
||||
/** Check if an inventory has at least one of a specific itemId. */
|
||||
bool hasItem(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId) const;
|
||||
|
||||
/** Check if an inventory has at least one of a specific itemName. */
|
||||
bool hasItemByName(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemName) const;
|
||||
|
||||
/** Count how many of a specific itemId are in an inventory. */
|
||||
int countItem(flecs::entity inventoryEntity,
|
||||
const Ogre::String &itemId) const;
|
||||
|
||||
/** Drop an item from inventory into the world at a position. */
|
||||
bool dropItem(flecs::entity inventoryEntity, int slotIndex,
|
||||
const Ogre::Vector3 &worldPosition, int count = 1);
|
||||
|
||||
/** Use an item from inventory (executes its use action). */
|
||||
bool useItem(flecs::entity characterEntity,
|
||||
flecs::entity inventoryEntity, int slotIndex);
|
||||
|
||||
/**
|
||||
* Pick up a world item entity into a character's inventory.
|
||||
* Called by ActuatorSystem when player presses E near an item.
|
||||
* Returns true if the item was picked up.
|
||||
*/
|
||||
bool pickupItem(flecs::entity characterEntity,
|
||||
flecs::entity itemEntity);
|
||||
|
||||
private:
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
EditorApp *m_editorApp;
|
||||
BehaviorTreeSystem *m_btSystem;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ITEM_SYSTEM_HPP
|
||||
@@ -34,6 +34,8 @@
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
@@ -318,6 +320,14 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
|
||||
json["behaviorTree"] = serializeBehaviorTree(entity);
|
||||
}
|
||||
|
||||
if (entity.has<ItemComponent>()) {
|
||||
json["item"] = serializeItem(entity);
|
||||
}
|
||||
|
||||
if (entity.has<InventoryComponent>()) {
|
||||
json["inventory"] = serializeInventory(entity);
|
||||
}
|
||||
|
||||
if (entity.has<PrefabInstanceComponent>()) {
|
||||
json["prefabInstance"] = serializePrefabInstance(entity);
|
||||
}
|
||||
@@ -530,6 +540,14 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
|
||||
deserializeBehaviorTree(entity, json["behaviorTree"]);
|
||||
}
|
||||
|
||||
if (json.contains("item")) {
|
||||
deserializeItem(entity, json["item"]);
|
||||
}
|
||||
|
||||
if (json.contains("inventory")) {
|
||||
deserializeInventory(entity, json["inventory"]);
|
||||
}
|
||||
|
||||
if (json.contains("sun")) {
|
||||
deserializeSun(entity, json["sun"]);
|
||||
}
|
||||
@@ -570,14 +588,13 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json,
|
||||
entity.child_of(parent);
|
||||
}
|
||||
|
||||
deserializeEntityComponents(entity, json, parent, uiSystem,
|
||||
true, true, true);
|
||||
deserializeEntityComponents(entity, json, parent, uiSystem, true, true,
|
||||
true);
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeEntityComponents(
|
||||
flecs::entity entity, const nlohmann::json &json,
|
||||
flecs::entity parent, EditorUISystem *uiSystem,
|
||||
bool processTransform, bool processName,
|
||||
flecs::entity entity, const nlohmann::json &json, flecs::entity parent,
|
||||
EditorUISystem *uiSystem, bool processTransform, bool processName,
|
||||
bool addEditorMarker)
|
||||
{
|
||||
if (processName) {
|
||||
@@ -631,8 +648,7 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
}
|
||||
|
||||
if (json.contains("proceduralTexture")) {
|
||||
deserializeProceduralTexture(entity,
|
||||
json["proceduralTexture"]);
|
||||
deserializeProceduralTexture(entity, json["proceduralTexture"]);
|
||||
}
|
||||
|
||||
if (json.contains("proceduralMaterial")) {
|
||||
@@ -666,8 +682,7 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
}
|
||||
|
||||
if (json.contains("playerController")) {
|
||||
deserializePlayerController(entity,
|
||||
json["playerController"]);
|
||||
deserializePlayerController(entity, json["playerController"]);
|
||||
}
|
||||
|
||||
if (json.contains("triangleBuffer")) {
|
||||
@@ -765,8 +780,7 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
deserializeRoof(entity, json["roof"]);
|
||||
}
|
||||
if (json.contains("furnitureTemplate")) {
|
||||
deserializeFurnitureTemplate(entity,
|
||||
json["furnitureTemplate"]);
|
||||
deserializeFurnitureTemplate(entity, json["furnitureTemplate"]);
|
||||
}
|
||||
if (json.contains("clearArea")) {
|
||||
deserializeClearArea(entity, json["clearArea"]);
|
||||
@@ -777,8 +791,8 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
}
|
||||
|
||||
if (json.contains("navMeshGeometrySource")) {
|
||||
deserializeNavMeshGeometrySource(
|
||||
entity, json["navMeshGeometrySource"]);
|
||||
deserializeNavMeshGeometrySource(entity,
|
||||
json["navMeshGeometrySource"]);
|
||||
}
|
||||
|
||||
if (json.contains("buoyancyInfo")) {
|
||||
@@ -818,6 +832,14 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
deserializeBehaviorTree(entity, json["behaviorTree"]);
|
||||
}
|
||||
|
||||
if (json.contains("item")) {
|
||||
deserializeItem(entity, json["item"]);
|
||||
}
|
||||
|
||||
if (json.contains("inventory")) {
|
||||
deserializeInventory(entity, json["inventory"]);
|
||||
}
|
||||
|
||||
if (json.contains("sun")) {
|
||||
deserializeSun(entity, json["sun"]);
|
||||
}
|
||||
@@ -860,8 +882,8 @@ nlohmann::json SceneSerializer::serializePrefabInstance(flecs::entity entity)
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializePrefabInstance(
|
||||
flecs::entity entity, const nlohmann::json &json)
|
||||
void SceneSerializer::deserializePrefabInstance(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
PrefabInstanceComponent prefab;
|
||||
prefab.prefabPath = json.value("prefabPath", "");
|
||||
@@ -892,8 +914,8 @@ bool SceneSerializer::savePrefab(flecs::entity rootEntity,
|
||||
}
|
||||
}
|
||||
|
||||
flecs::entity SceneSerializer::loadPrefabForEdit(
|
||||
const std::string &filepath, EditorUISystem *uiSystem)
|
||||
flecs::entity SceneSerializer::loadPrefabForEdit(const std::string &filepath,
|
||||
EditorUISystem *uiSystem)
|
||||
{
|
||||
try {
|
||||
std::ifstream file(filepath);
|
||||
@@ -918,9 +940,8 @@ flecs::entity SceneSerializer::loadPrefabForEdit(
|
||||
}
|
||||
|
||||
deserializeEntityComponents(entity, prefabJson,
|
||||
flecs::entity::null(),
|
||||
uiSystem, true, true,
|
||||
true);
|
||||
flecs::entity::null(), uiSystem,
|
||||
true, true, true);
|
||||
|
||||
return entity;
|
||||
} catch (const std::exception &e) {
|
||||
@@ -958,17 +979,16 @@ bool SceneSerializer::instantiatePrefab(flecs::entity instanceEntity,
|
||||
// is the world-space override.
|
||||
// Skip name — the instance entity keeps its own name.
|
||||
flecs::entity parent = instanceEntity.parent();
|
||||
deserializeEntityComponents(instanceEntity, prefabJson,
|
||||
parent, uiSystem, false,
|
||||
false, false);
|
||||
deserializeEntityComponents(instanceEntity, prefabJson, parent,
|
||||
uiSystem, false, false, false);
|
||||
|
||||
// Restore main scene entity map
|
||||
m_entityMap = savedMap;
|
||||
|
||||
return true;
|
||||
} catch (const std::exception &e) {
|
||||
m_lastError = std::string("Prefab instantiate error: ") +
|
||||
e.what();
|
||||
m_lastError =
|
||||
std::string("Prefab instantiate error: ") + e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2920,9 +2940,12 @@ void SceneSerializer::deserializePlayerController(flecs::entity entity,
|
||||
pc.swimIdleState = json.value("swimIdleState", pc.swimIdleState);
|
||||
pc.swimState = json.value("swimState", pc.swimState);
|
||||
pc.swimFastState = json.value("swimFastState", pc.swimFastState);
|
||||
pc.actuatorDistance = json.value("actuatorDistance", pc.actuatorDistance);
|
||||
pc.actuatorCooldown = json.value("actuatorCooldown", pc.actuatorCooldown);
|
||||
if (json.contains("actuatorColor") && json["actuatorColor"].is_array() &&
|
||||
pc.actuatorDistance =
|
||||
json.value("actuatorDistance", pc.actuatorDistance);
|
||||
pc.actuatorCooldown =
|
||||
json.value("actuatorCooldown", pc.actuatorCooldown);
|
||||
if (json.contains("actuatorColor") &&
|
||||
json["actuatorColor"].is_array() &&
|
||||
json["actuatorColor"].size() >= 3) {
|
||||
pc.actuatorColor = Ogre::Vector3(json["actuatorColor"][0],
|
||||
json["actuatorColor"][1],
|
||||
@@ -2930,7 +2953,8 @@ void SceneSerializer::deserializePlayerController(flecs::entity entity,
|
||||
}
|
||||
pc.distantCircleRadius =
|
||||
json.value("distantCircleRadius", pc.distantCircleRadius);
|
||||
pc.nearCircleRadius = json.value("nearCircleRadius", pc.nearCircleRadius);
|
||||
pc.nearCircleRadius =
|
||||
json.value("nearCircleRadius", pc.nearCircleRadius);
|
||||
pc.actuatorLabelFontSize =
|
||||
json.value("actuatorLabelFontSize", pc.actuatorLabelFontSize);
|
||||
// inputLocked is runtime-only, always reset to false on load
|
||||
@@ -3472,7 +3496,7 @@ nlohmann::json SceneSerializer::serializePathFollowing(flecs::entity entity)
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeActionDebug(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
ActionDebug debug;
|
||||
if (json.contains("blackboard"))
|
||||
@@ -3483,7 +3507,7 @@ void SceneSerializer::deserializeActionDebug(flecs::entity entity,
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializePathFollowing(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
PathFollowingComponent pf;
|
||||
if (json.contains("pathFollowingStates") &&
|
||||
@@ -3566,7 +3590,8 @@ void SceneSerializer::deserializeSmartObject(flecs::entity entity,
|
||||
|
||||
nlohmann::json SceneSerializer::serializeGoapPlanner(flecs::entity entity)
|
||||
{
|
||||
const GoapPlannerComponent &planner = entity.get<GoapPlannerComponent>();
|
||||
const GoapPlannerComponent &planner =
|
||||
entity.get<GoapPlannerComponent>();
|
||||
nlohmann::json json;
|
||||
json["actionNames"] = planner.actionNames;
|
||||
if (!planner.goalNames.empty())
|
||||
@@ -3684,7 +3709,6 @@ void SceneSerializer::deserializeNavMeshGeometrySource(
|
||||
entity.set<NavMeshGeometrySource>(src);
|
||||
}
|
||||
|
||||
|
||||
nlohmann::json SceneSerializer::serializeActuator(flecs::entity entity)
|
||||
{
|
||||
const ActuatorComponent &actuator = entity.get<ActuatorComponent>();
|
||||
@@ -3711,10 +3735,10 @@ void SceneSerializer::deserializeActuator(flecs::entity entity,
|
||||
entity.set<ActuatorComponent>(actuator);
|
||||
}
|
||||
|
||||
|
||||
nlohmann::json SceneSerializer::serializeEventHandler(flecs::entity entity)
|
||||
{
|
||||
const EventHandlerComponent &handler = entity.get<EventHandlerComponent>();
|
||||
const EventHandlerComponent &handler =
|
||||
entity.get<EventHandlerComponent>();
|
||||
nlohmann::json json;
|
||||
json["eventName"] = handler.eventName;
|
||||
json["actionName"] = handler.actionName;
|
||||
@@ -3731,3 +3755,93 @@ void SceneSerializer::deserializeEventHandler(flecs::entity entity,
|
||||
handler.enabled = json.value("enabled", handler.enabled);
|
||||
entity.set<EventHandlerComponent>(handler);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeItem(flecs::entity entity)
|
||||
{
|
||||
const ItemComponent &item = entity.get<ItemComponent>();
|
||||
nlohmann::json json;
|
||||
json["itemName"] = item.itemName;
|
||||
json["itemType"] = item.itemType;
|
||||
json["itemId"] = item.itemId;
|
||||
json["stackSize"] = item.stackSize;
|
||||
json["maxStackSize"] = item.maxStackSize;
|
||||
json["weight"] = item.weight;
|
||||
json["value"] = item.value;
|
||||
json["useActionName"] = item.useActionName;
|
||||
return json;
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeInventory(flecs::entity entity)
|
||||
{
|
||||
const InventoryComponent &inv = entity.get<InventoryComponent>();
|
||||
nlohmann::json json;
|
||||
json["maxSlots"] = inv.maxSlots;
|
||||
json["maxWeight"] = inv.maxWeight;
|
||||
json["isContainer"] = inv.isContainer;
|
||||
json["isOpen"] = inv.isOpen;
|
||||
|
||||
nlohmann::json slotsJson = nlohmann::json::array();
|
||||
for (const auto &slot : inv.slots) {
|
||||
nlohmann::json slotJson;
|
||||
slotJson["itemEntity"] = (uint64_t)slot.itemEntity;
|
||||
slotJson["itemName"] = slot.itemName;
|
||||
slotJson["itemType"] = slot.itemType;
|
||||
slotJson["itemId"] = slot.itemId;
|
||||
slotJson["stackSize"] = slot.stackSize;
|
||||
slotJson["maxStackSize"] = slot.maxStackSize;
|
||||
slotJson["weight"] = slot.weight;
|
||||
slotJson["value"] = slot.value;
|
||||
slotJson["useActionName"] = slot.useActionName;
|
||||
slotsJson.push_back(slotJson);
|
||||
}
|
||||
json["slots"] = slotsJson;
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeItem(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
ItemComponent item;
|
||||
item.itemName = json.value("itemName", "Item");
|
||||
item.itemType = json.value("itemType", "misc");
|
||||
item.itemId = json.value("itemId", "");
|
||||
item.stackSize = json.value("stackSize", 1);
|
||||
item.maxStackSize = json.value("maxStackSize", 99);
|
||||
item.weight = json.value("weight", 0.1f);
|
||||
item.value = json.value("value", 1);
|
||||
item.useActionName = json.value("useActionName", "");
|
||||
entity.set<ItemComponent>(item);
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeInventory(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
InventoryComponent inv;
|
||||
inv.maxSlots = json.value("maxSlots", 20);
|
||||
inv.maxWeight = json.value("maxWeight", 50.0f);
|
||||
inv.isContainer = json.value("isContainer", false);
|
||||
inv.isOpen = json.value("isOpen", false);
|
||||
|
||||
inv.slots.clear();
|
||||
if (json.contains("slots") && json["slots"].is_array()) {
|
||||
for (const auto &slotJson : json["slots"]) {
|
||||
InventorySlot slot;
|
||||
slot.itemEntity = (flecs::entity_t)slotJson.value(
|
||||
"itemEntity", (uint64_t)0);
|
||||
slot.itemName = slotJson.value("itemName", "");
|
||||
slot.itemType = slotJson.value("itemType", "");
|
||||
slot.itemId = slotJson.value("itemId", "");
|
||||
slot.stackSize = slotJson.value("stackSize", 0);
|
||||
slot.maxStackSize = slotJson.value("maxStackSize", 99);
|
||||
slot.weight = slotJson.value("weight", 0.1f);
|
||||
slot.value = slotJson.value("value", 1);
|
||||
slot.useActionName =
|
||||
slotJson.value("useActionName", "");
|
||||
inv.slots.push_back(slot);
|
||||
}
|
||||
}
|
||||
|
||||
inv.recalculateWeight();
|
||||
entity.set<InventoryComponent>(inv);
|
||||
}
|
||||
|
||||
@@ -40,8 +40,7 @@ public:
|
||||
/**
|
||||
* Save an entity subtree as a prefab JSON file.
|
||||
*/
|
||||
bool savePrefab(flecs::entity rootEntity,
|
||||
const std::string &filepath);
|
||||
bool savePrefab(flecs::entity rootEntity, const std::string &filepath);
|
||||
|
||||
/**
|
||||
* Instantiate a prefab onto an existing entity.
|
||||
@@ -205,6 +204,13 @@ private:
|
||||
void deserializeSkybox(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
|
||||
// Item/Inventory serialization
|
||||
nlohmann::json serializeItem(flecs::entity entity);
|
||||
nlohmann::json serializeInventory(flecs::entity entity);
|
||||
void deserializeItem(flecs::entity entity, const nlohmann::json &json);
|
||||
void deserializeInventory(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
|
||||
// AI/GOAP serialization
|
||||
nlohmann::json serializeActionDatabase(flecs::entity entity);
|
||||
nlohmann::json serializeActionDebug(flecs::entity entity);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
class NavMeshSystem;
|
||||
class BehaviorTreeSystem;
|
||||
class AnimationTreeSystem;
|
||||
class ItemSystem;
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
@@ -55,6 +56,22 @@ public:
|
||||
m_editorApp = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ItemSystem for behavior tree item/inventory nodes.
|
||||
*/
|
||||
void setItemSystem(ItemSystem *system)
|
||||
{
|
||||
m_itemSystem = system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ItemSystem for behavior tree item/inventory nodes.
|
||||
*/
|
||||
ItemSystem *getItemSystem() const
|
||||
{
|
||||
return m_itemSystem;
|
||||
}
|
||||
|
||||
void update(float deltaTime);
|
||||
|
||||
/**
|
||||
@@ -121,6 +138,7 @@ private:
|
||||
BehaviorTreeSystem *m_btSystem;
|
||||
AnimationTreeSystem *m_animTreeSystem;
|
||||
EditorApp *m_editorApp = nullptr;
|
||||
ItemSystem *m_itemSystem = nullptr;
|
||||
|
||||
std::unordered_map<flecs::entity_t, CharacterState> m_states;
|
||||
|
||||
|
||||
93
src/features/editScene/ui/DialogueEditor.cpp
Normal file
93
src/features/editScene/ui/DialogueEditor.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
#include "DialogueEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
bool DialogueEditor::renderComponent(flecs::entity entity,
|
||||
DialogueComponent &dc)
|
||||
{
|
||||
(void)entity;
|
||||
bool modified = false;
|
||||
|
||||
if (ImGui::CollapsingHeader("Dialogue Box",
|
||||
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::Indent();
|
||||
|
||||
// State display (read-only)
|
||||
const char *stateNames[] = { "Idle", "Showing",
|
||||
"AwaitingChoice" };
|
||||
ImGui::Text("State: %s", stateNames[(int)dc.state]);
|
||||
ImGui::Separator();
|
||||
|
||||
// Font configuration
|
||||
char fontNameBuf[256];
|
||||
snprintf(fontNameBuf, sizeof(fontNameBuf), "%s",
|
||||
dc.fontName.c_str());
|
||||
if (ImGui::InputText("Font Name", fontNameBuf,
|
||||
sizeof(fontNameBuf))) {
|
||||
dc.fontName = fontNameBuf;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::DragFloat("Font Size", &dc.fontSize, 0.5f, 8.0f,
|
||||
128.0f)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::DragFloat("Speaker Font Size", &dc.speakerFontSize,
|
||||
0.5f, 8.0f, 128.0f)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Visual configuration
|
||||
if (ImGui::SliderFloat("Background Opacity",
|
||||
&dc.backgroundOpacity, 0.0f, 1.0f)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::SliderFloat("Box Height Fraction",
|
||||
&dc.boxHeightFraction, 0.1f, 0.5f)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::SliderFloat("Box Position Fraction",
|
||||
&dc.boxPositionFraction, 0.0f, 1.0f)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Enabled toggle
|
||||
if (ImGui::Checkbox("Enabled", &dc.enabled))
|
||||
modified = true;
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Test buttons (only in editor mode)
|
||||
if (ImGui::Button("Test: Show Sample Text")) {
|
||||
dc.show("This is a sample narration text for testing the dialogue box layout. "
|
||||
"It should wrap properly within the box.",
|
||||
{}, "Test Speaker");
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::Button("Test: Show With Choices")) {
|
||||
std::vector<Ogre::String> testChoices = {
|
||||
"Option 1: Go left", "Option 2: Go right",
|
||||
"Option 3: Stay"
|
||||
};
|
||||
dc.show("What would you like to do?", testChoices,
|
||||
"Narrator");
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::Button("Reset Dialogue")) {
|
||||
dc.reset();
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
21
src/features/editScene/ui/DialogueEditor.hpp
Normal file
21
src/features/editScene/ui/DialogueEditor.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef EDITSCENE_DIALOGUEEDITOR_HPP
|
||||
#define EDITSCENE_DIALOGUEEDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/DialogueComponent.hpp"
|
||||
|
||||
/**
|
||||
* Editor for DialogueComponent
|
||||
*/
|
||||
class DialogueEditor : public ComponentEditor<DialogueComponent> {
|
||||
public:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
DialogueComponent &dc) override;
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Dialogue Box";
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DIALOGUEEDITOR_HPP
|
||||
63
src/features/editScene/ui/InventoryEditor.cpp
Normal file
63
src/features/editScene/ui/InventoryEditor.cpp
Normal file
@@ -0,0 +1,63 @@
|
||||
#include "InventoryEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
bool InventoryEditor::renderComponent(flecs::entity entity,
|
||||
InventoryComponent &inventory)
|
||||
{
|
||||
(void)entity;
|
||||
bool modified = false;
|
||||
ImGui::PushID("Inventory");
|
||||
|
||||
ImGui::Text("Inventory Settings");
|
||||
ImGui::Separator();
|
||||
|
||||
// Max slots
|
||||
int maxSlots = inventory.maxSlots;
|
||||
if (ImGui::DragInt("Max Slots", &maxSlots, 1, 1, 999)) {
|
||||
if (maxSlots < 1)
|
||||
maxSlots = 1;
|
||||
inventory.maxSlots = maxSlots;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Max weight
|
||||
if (ImGui::DragFloat("Max Weight", &inventory.maxWeight, 0.1f, 0.0f,
|
||||
10000.0f, "%.1f")) {
|
||||
if (inventory.maxWeight < 0.0f)
|
||||
inventory.maxWeight = 0.0f;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Is container
|
||||
bool isContainer = inventory.isContainer;
|
||||
if (ImGui::Checkbox("Is Container", &isContainer)) {
|
||||
inventory.isContainer = isContainer;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Contents (%d items, %.1f weight)", inventory.countItems(),
|
||||
inventory.totalWeight);
|
||||
|
||||
// List slots
|
||||
for (int i = 0; i < (int)inventory.slots.size(); i++) {
|
||||
auto &slot = inventory.slots[i];
|
||||
if (slot.isEmpty())
|
||||
continue;
|
||||
|
||||
ImGui::PushID(i);
|
||||
ImGui::Text("[%d] %s x%d (%s)", i, slot.itemName.c_str(),
|
||||
slot.stackSize, slot.itemType.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
slot.clear();
|
||||
modified = true;
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
return modified;
|
||||
}
|
||||
20
src/features/editScene/ui/InventoryEditor.hpp
Normal file
20
src/features/editScene/ui/InventoryEditor.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef EDITSCENE_INVENTORY_EDITOR_HPP
|
||||
#define EDITSCENE_INVENTORY_EDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
|
||||
class InventoryEditor : public ComponentEditor<InventoryComponent> {
|
||||
public:
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Inventory";
|
||||
}
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
InventoryComponent &inventory) override;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_INVENTORY_EDITOR_HPP
|
||||
130
src/features/editScene/ui/ItemEditor.cpp
Normal file
130
src/features/editScene/ui/ItemEditor.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "ItemEditor.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
bool ItemEditor::renderComponent(flecs::entity entity, ItemComponent &item)
|
||||
{
|
||||
(void)entity;
|
||||
bool modified = false;
|
||||
ImGui::PushID("Item");
|
||||
|
||||
ImGui::Text("Item Settings");
|
||||
ImGui::Separator();
|
||||
|
||||
// Item name
|
||||
char nameBuf[256];
|
||||
strncpy(nameBuf, item.itemName.c_str(), sizeof(nameBuf));
|
||||
nameBuf[sizeof(nameBuf) - 1] = '\0';
|
||||
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) {
|
||||
item.itemName = nameBuf;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Item type
|
||||
char typeBuf[256];
|
||||
strncpy(typeBuf, item.itemType.c_str(), sizeof(typeBuf));
|
||||
typeBuf[sizeof(typeBuf) - 1] = '\0';
|
||||
if (ImGui::InputText("Type", typeBuf, sizeof(typeBuf))) {
|
||||
item.itemType = typeBuf;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Item ID
|
||||
char idBuf[256];
|
||||
strncpy(idBuf, item.itemId.c_str(), sizeof(idBuf));
|
||||
idBuf[sizeof(idBuf) - 1] = '\0';
|
||||
if (ImGui::InputText("Item ID", idBuf, sizeof(idBuf))) {
|
||||
item.itemId = idBuf;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Stack size
|
||||
int stackSize = item.stackSize;
|
||||
if (ImGui::DragInt("Stack Size", &stackSize, 1, 1, 999)) {
|
||||
if (stackSize < 1)
|
||||
stackSize = 1;
|
||||
item.stackSize = stackSize;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Max stack size
|
||||
int maxStack = item.maxStackSize;
|
||||
if (ImGui::DragInt("Max Stack", &maxStack, 1, 1, 999)) {
|
||||
if (maxStack < 1)
|
||||
maxStack = 1;
|
||||
item.maxStackSize = maxStack;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Weight
|
||||
if (ImGui::DragFloat("Weight", &item.weight, 0.01f, 0.0f, 100.0f,
|
||||
"%.2f")) {
|
||||
if (item.weight < 0.0f)
|
||||
item.weight = 0.0f;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Value
|
||||
if (ImGui::DragInt("Value", &item.value, 1, 0, 99999)) {
|
||||
if (item.value < 0)
|
||||
item.value = 0;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Use Action");
|
||||
|
||||
// Use action name
|
||||
char useBuf[256];
|
||||
strncpy(useBuf, item.useActionName.c_str(), sizeof(useBuf));
|
||||
useBuf[sizeof(useBuf) - 1] = '\0';
|
||||
if (ImGui::InputText("Use Action", useBuf, sizeof(useBuf))) {
|
||||
item.useActionName = useBuf;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Pick from action database
|
||||
ActionDatabase *db = nullptr;
|
||||
auto world = entity.world();
|
||||
world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
|
||||
if (db && !db->actions.empty()) {
|
||||
static int selectedAction = -1;
|
||||
std::vector<const char *> availableNames;
|
||||
std::vector<Ogre::String> availableNamesStorage;
|
||||
|
||||
for (const auto &action : db->actions) {
|
||||
availableNamesStorage.push_back(action.name);
|
||||
availableNames.push_back(
|
||||
availableNamesStorage.back().c_str());
|
||||
}
|
||||
|
||||
if (!availableNames.empty()) {
|
||||
if (selectedAction >= (int)availableNames.size())
|
||||
selectedAction = 0;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Combo("##useActionSelect", &selectedAction,
|
||||
availableNames.data(),
|
||||
(int)availableNames.size())) {
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Set")) {
|
||||
if (selectedAction >= 0 &&
|
||||
selectedAction <
|
||||
(int)availableNames.size()) {
|
||||
item.useActionName =
|
||||
availableNamesStorage
|
||||
[selectedAction];
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
return modified;
|
||||
}
|
||||
20
src/features/editScene/ui/ItemEditor.hpp
Normal file
20
src/features/editScene/ui/ItemEditor.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef EDITSCENE_ITEM_EDITOR_HPP
|
||||
#define EDITSCENE_ITEM_EDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
|
||||
class ItemEditor : public ComponentEditor<ItemComponent> {
|
||||
public:
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Item";
|
||||
}
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
ItemComponent &item) override;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ITEM_EDITOR_HPP
|
||||
Reference in New Issue
Block a user