Compare commits

...

2 Commits

Author SHA1 Message Date
02fa78764a Lua API implemented 2026-04-29 14:13:50 +03:00
abe6eef6b3 Modules update 2026-04-29 12:53:13 +03:00
35 changed files with 5311 additions and 142 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,131 @@
#ifndef EDITSCENE_DIALOGUE_COMPONENT_HPP
#define EDITSCENE_DIALOGUE_COMPONENT_HPP
#pragma once
#include <Ogre.h>
#include <functional>
#include <string>
#include <vector>
/**
* Visual-novel style dialogue box component.
*
* Displays a narration text box at the bottom of the screen with optional
* player choices. The dialogue can be driven via the EventBus system
* (using "dialogue_show" event) or directly via the component API.
*
* Only active in game mode (GamePlayState::Playing).
*
* Event payload (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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -0,0 +1,236 @@
#include "LuaEntityApi.hpp"
#include "components/EditorMarker.hpp"
#include <OgreLogManager.h>
#include <iostream>
namespace editScene
{
// Global entity ID map
LuaEntityIdMap g_luaEntityIdMap;
int luaEntityToId(flecs::entity e)
{
if (!e.is_valid())
return -1;
return g_luaEntityIdMap.addEntity(e);
}
flecs::entity luaIdToEntity(int id)
{
return g_luaEntityIdMap.getEntity(id);
}
// ---------------------------------------------------------------------------
// Helper: get the Flecs world from the Lua registry
// ---------------------------------------------------------------------------
static flecs::world getWorld(lua_State *L)
{
lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld");
OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered");
flecs::world *world =
static_cast<flecs::world *>(lua_touserdata(L, -1));
lua_pop(L, 1);
return *world;
}
// ---------------------------------------------------------------------------
// Lua: ecs.create_entity() -> int (entity ID)
// Creates a new entity with EditorMarkerComponent and returns its Lua ID.
// ---------------------------------------------------------------------------
static int luaCreateEntity(lua_State *L)
{
flecs::world world = getWorld(L);
flecs::entity e = world.entity();
// Add EditorMarkerComponent so it appears in the editor hierarchy
// (the component is forward-declared; we use a tag approach)
e.add<EditorMarkerComponent>();
int id = g_luaEntityIdMap.addEntity(e);
lua_pushinteger(L, id);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.destroy_entity(id) -> nil
// Destroys an entity and removes it from the ID map.
// ---------------------------------------------------------------------------
static int luaDestroyEntity(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
int id = lua_tointeger(L, 1);
flecs::entity e = g_luaEntityIdMap.getEntity(id);
if (e.is_alive()) {
g_luaEntityIdMap.removeEntity(e);
e.destruct();
}
return 0;
}
// ---------------------------------------------------------------------------
// Lua: ecs.entity_exists(id) -> bool
// ---------------------------------------------------------------------------
static int luaEntityExists(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
int id = lua_tointeger(L, 1);
lua_pushboolean(L, g_luaEntityIdMap.hasId(id) ? 1 : 0);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.get_player_entity() -> int (entity ID) or nil
// Looks up the entity named "player" in the Flecs world.
// ---------------------------------------------------------------------------
static int luaGetPlayerEntity(lua_State *L)
{
flecs::world world = getWorld(L);
flecs::entity e = world.lookup("player");
if (e.is_valid()) {
int id = g_luaEntityIdMap.addEntity(e);
lua_pushinteger(L, id);
} else {
lua_pushnil(L);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.get_entity_by_name(name) -> int (entity ID) or nil
// ---------------------------------------------------------------------------
static int luaGetEntityByName(lua_State *L)
{
luaL_checktype(L, 1, LUA_TSTRING);
const char *name = lua_tostring(L, 1);
flecs::world world = getWorld(L);
flecs::entity e = world.lookup(name);
if (e.is_valid()) {
int id = g_luaEntityIdMap.addEntity(e);
lua_pushinteger(L, id);
} else {
lua_pushnil(L);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.set_entity_name(id, name) -> nil
// ---------------------------------------------------------------------------
static int luaSetEntityName(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
luaL_checktype(L, 2, LUA_TSTRING);
int id = lua_tointeger(L, 1);
const char *name = lua_tostring(L, 2);
flecs::entity e = g_luaEntityIdMap.getEntity(id);
e.set_name(name);
return 0;
}
// ---------------------------------------------------------------------------
// Lua: ecs.get_entity_name(id) -> string or nil
// ---------------------------------------------------------------------------
static int luaGetEntityName(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
int id = lua_tointeger(L, 1);
flecs::entity e = g_luaEntityIdMap.getEntity(id);
const char *name = e.name();
if (name && name[0]) {
lua_pushstring(L, name);
} else {
lua_pushnil(L);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.parent(id) -> int (parent ID) or nil
// ---------------------------------------------------------------------------
static int luaGetParent(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
int id = lua_tointeger(L, 1);
flecs::entity e = g_luaEntityIdMap.getEntity(id);
flecs::entity parent = e.parent();
if (parent.is_valid()) {
int parentId = g_luaEntityIdMap.addEntity(parent);
lua_pushinteger(L, parentId);
} else {
lua_pushnil(L);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.children(id) -> table of child IDs
// ---------------------------------------------------------------------------
static int luaGetChildren(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
int id = lua_tointeger(L, 1);
flecs::entity e = g_luaEntityIdMap.getEntity(id);
lua_newtable(L); // result table
int index = 1;
e.children([&](flecs::entity child) {
int childId = g_luaEntityIdMap.addEntity(child);
lua_pushinteger(L, childId);
lua_rawseti(L, -2, index);
index++;
});
return 1;
}
// ---------------------------------------------------------------------------
// Register all entity API functions
// ---------------------------------------------------------------------------
void registerLuaEntityApi(lua_State *L)
{
// Create the "ecs" global table
lua_newtable(L);
// Entity management
lua_pushcfunction(L, luaCreateEntity);
lua_setfield(L, -2, "create_entity");
lua_pushcfunction(L, luaDestroyEntity);
lua_setfield(L, -2, "destroy_entity");
lua_pushcfunction(L, luaEntityExists);
lua_setfield(L, -2, "entity_exists");
lua_pushcfunction(L, luaGetPlayerEntity);
lua_setfield(L, -2, "get_player_entity");
lua_pushcfunction(L, luaGetEntityByName);
lua_setfield(L, -2, "get_entity_by_name");
lua_pushcfunction(L, luaSetEntityName);
lua_setfield(L, -2, "set_entity_name");
lua_pushcfunction(L, luaGetEntityName);
lua_setfield(L, -2, "get_entity_name");
// Hierarchy
lua_pushcfunction(L, luaGetParent);
lua_setfield(L, -2, "parent");
lua_pushcfunction(L, luaGetChildren);
lua_setfield(L, -2, "children");
// Set the global
lua_setglobal(L, "ecs");
}
} // namespace editScene

View File

@@ -0,0 +1,126 @@
#ifndef EDITSCENE_LUA_ENTITY_API_HPP
#define EDITSCENE_LUA_ENTITY_API_HPP
#pragma once
#include <flecs.h>
#include <lua.hpp>
#include <Ogre.h>
#include <unordered_map>
/**
* @file LuaEntityApi.hpp
* @brief Lua API for entity creation, destruction, and ID mapping.
*
* Provides a bidirectional mapping between Lua integer IDs and
* Flecs entity handles. Lua scripts reference entities by integer
* IDs rather than raw Flecs handles for safety and simplicity.
*
* Exposed Lua globals:
* ecs.create_entity() -> int (entity ID)
* ecs.destroy_entity(id) -> nil
* ecs.entity_exists(id) -> bool
* ecs.get_player_entity() -> int (entity ID)
* ecs.get_entity_by_name(name) -> int (entity ID) or nil
* ecs.set_entity_name(id, name)-> nil
* ecs.get_entity_name(id) -> string or nil
* ecs.parent(id) -> int (parent ID) or nil
* ecs.children(id) -> table of child IDs
*/
namespace editScene
{
/**
* @brief Global bidirectional mapping between Lua integer IDs and Flecs entities.
*
* This is a singleton-like global so that all Lua API functions can
* access it without needing to pass it through every closure.
*/
struct LuaEntityIdMap {
std::unordered_map<int, flecs::entity> id2entity;
std::unordered_map<flecs::entity_t, int> entity2id;
int nextId = 0;
/** @brief Get the next available integer ID. */
int getNextId()
{
nextId++;
return nextId;
}
/**
* @brief Add an entity to the map, returning its integer ID.
* If the entity is already mapped, returns the existing ID.
*/
int addEntity(flecs::entity e)
{
if (entity2id.find(e.id()) != entity2id.end())
return entity2id[e.id()];
int id = getNextId();
id2entity[id] = e;
entity2id[e.id()] = id;
return id;
}
/**
* @brief Get the Flecs entity for an integer ID.
* Asserts if the ID is not found or the entity is invalid.
*/
flecs::entity getEntity(int id)
{
auto it = id2entity.find(id);
OgreAssert(it != id2entity.end(), "Invalid entity ID");
OgreAssert(it->second.is_valid(), "Entity is no longer valid");
return it->second;
}
/**
* @brief Remove an entity from the map.
*/
void removeEntity(flecs::entity e)
{
auto it = entity2id.find(e.id());
if (it != entity2id.end()) {
id2entity.erase(it->second);
entity2id.erase(it);
}
}
/**
* @brief Check if an integer ID is valid.
*/
bool hasId(int id) const
{
auto it = id2entity.find(id);
return it != id2entity.end() && it->second.is_valid();
}
};
/** @brief Global entity ID map instance. */
extern LuaEntityIdMap g_luaEntityIdMap;
/**
* @brief Convert a Flecs entity to a Lua integer ID.
* @return The integer ID, or -1 if the entity is invalid.
*/
int luaEntityToId(flecs::entity e);
/**
* @brief Convert a Lua integer ID to a Flecs entity.
* Asserts if the ID is invalid.
*/
flecs::entity luaIdToEntity(int id);
/**
* @brief Register all entity-related Lua API functions into the global table.
*
* Creates the "ecs" global table (or adds to it) with entity management
* functions. Must be called after LuaState is constructed.
*
* @param L The Lua state.
*/
void registerLuaEntityApi(lua_State *L);
} // namespace editScene
#endif // EDITSCENE_LUA_ENTITY_API_HPP

View File

@@ -0,0 +1,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 &params) {
// Push the Lua callback function
lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef);
// Push event name
lua_pushstring(L, eventName.c_str());
// Push params as a Lua table
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

View 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

View File

@@ -0,0 +1,187 @@
#include "LuaState.hpp"
#include <OgreResourceGroupManager.h>
#include <OgreLogManager.h>
#include <OgreDataStream.h>
#include <iostream>
extern "C" {
int luaopen_lpeg(lua_State *L);
}
namespace editScene
{
// ---------------------------------------------------------------------------
// Custom library loader: loads .lua files from OGRE resource groups
// ---------------------------------------------------------------------------
int LuaState::luaLibraryLoader(lua_State *L)
{
if (!lua_isstring(L, 1)) {
luaL_error(
L,
"luaLibraryLoader: Expected string for first parameter");
}
std::string libraryFile = lua_tostring(L, 1);
// Translate '.' to '/' for OGRE resource path compatibility
while (libraryFile.find('.') != std::string::npos)
libraryFile.replace(libraryFile.find('.'), 1, "/");
libraryFile += ".lua";
Ogre::DataStreamPtr stream =
Ogre::ResourceGroupManager::getSingleton().openResource(
libraryFile, "LuaScripts");
Ogre::String script = stream->getAsString();
if (luaL_loadbuffer(L, script.c_str(), script.length(),
libraryFile.c_str())) {
luaL_error(
L,
"Error loading library '%s' from resource archive.\n%s",
libraryFile.c_str(), lua_tostring(L, -1));
}
return 1;
}
// ---------------------------------------------------------------------------
// Install the custom loader into package.searchers
// ---------------------------------------------------------------------------
void LuaState::installLibraryLoader()
{
lua_getglobal(L, "table");
lua_getfield(L, -1, "insert");
lua_remove(L, -2); // table
lua_getglobal(L, "package");
lua_getfield(L, -1, "searchers");
lua_remove(L, -2); // package
lua_pushnumber(L, 1); // insert at position 1 (highest priority)
lua_pushcfunction(L, luaLibraryLoader);
if (lua_pcall(L, 3, 0, 0))
Ogre::LogManager::getSingleton().stream() << lua_tostring(L, 1);
}
// ---------------------------------------------------------------------------
// Constructor: create Lua state and open standard libraries
// ---------------------------------------------------------------------------
LuaState::LuaState()
: L(luaL_newstate())
{
luaopen_base(L);
luaopen_table(L);
luaopen_package(L);
luaL_requiref(L, "table", luaopen_table, 1);
lua_pop(L, 1);
luaL_requiref(L, "math", luaopen_math, 1);
lua_pop(L, 1);
luaL_requiref(L, "package", luaopen_package, 1);
lua_pop(L, 1);
luaL_requiref(L, "string", luaopen_string, 1);
lua_pop(L, 1);
luaL_requiref(L, "io", luaopen_io, 1);
lua_pop(L, 1);
luaL_requiref(L, "lpeg", luaopen_lpeg, 1);
lua_pop(L, 1);
installLibraryLoader();
lua_pop(L, 1);
}
// ---------------------------------------------------------------------------
// Destructor
// ---------------------------------------------------------------------------
LuaState::~LuaState()
{
lua_close(L);
}
// ---------------------------------------------------------------------------
// Handler registration
// ---------------------------------------------------------------------------
int LuaState::setupHandler()
{
luaL_checktype(L, 1, LUA_TFUNCTION);
lua_pushvalue(L, 1);
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
setupHandlers.push_back(ref);
return 0;
}
// ---------------------------------------------------------------------------
// Call all handlers with an event name (no entities)
// ---------------------------------------------------------------------------
int LuaState::callHandler(const Ogre::String &event)
{
for (int ref : setupHandlers) {
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
lua_pushstring(L, event.c_str());
lua_pushinteger(L, -1);
lua_pushinteger(L, -1);
if (lua_pcall(L, 3, 0, 0) != LUA_OK) {
Ogre::LogManager::getSingleton().stream()
<< lua_tostring(L, -1);
OgreAssert(false, "Lua error");
}
}
return 0;
}
// ---------------------------------------------------------------------------
// Call all handlers with an event name and two entities
// ---------------------------------------------------------------------------
int LuaState::callHandler(const Ogre::String &event, flecs::entity e1,
flecs::entity e2)
{
// Entity IDs are mapped through the global idmap (see LuaEntityApi.cpp)
extern int luaEntityToId(flecs::entity e);
extern flecs::entity luaIdToEntity(int id);
for (int ref : setupHandlers) {
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
lua_pushstring(L, event.c_str());
lua_pushinteger(L, luaEntityToId(e1));
lua_pushinteger(L, luaEntityToId(e2));
if (lua_pcall(L, 3, 0, 0) != LUA_OK) {
Ogre::LogManager::getSingleton().stream()
<< lua_tostring(L, -1);
OgreAssert(false, "Lua error");
}
}
return 0;
}
// ---------------------------------------------------------------------------
// Late setup: load data.lua and run initialisation
// ---------------------------------------------------------------------------
void LuaState::lateSetup()
{
Ogre::DataStreamPtr stream =
Ogre::ResourceGroupManager::getSingleton().openResource(
"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

View File

@@ -0,0 +1,137 @@
#ifndef EDITSCENE_LUA_STATE_HPP
#define EDITSCENE_LUA_STATE_HPP
#pragma once
#include <Ogre.h>
#include <lua.hpp>
#include <flecs.h>
#include <string>
#include <vector>
/**
* @file LuaState.hpp
* @brief Lua state management for the editScene editor.
*
* Manages the Lua virtual machine instance, library loading,
* script loading from OGRE resource groups, and the custom
* package.searchers entry that loads Lua modules from
* the "LuaScripts" resource group.
*
* Usage:
* LuaState lua;
* lua.installLibraryLoader(); // Register custom searcher
* lua.lateSetup(); // Load data.lua and run startup
* lua.callHandler("event_name", e1, e2);
*/
namespace editScene
{
/**
* @brief Manages a single lua_State instance.
*
* Opens standard Lua libraries (base, table, math, package, string, io)
* plus LPEG. Installs a custom package.searchers entry that loads
* .lua files from OGRE's "LuaScripts" resource group.
*
* Provides a handler/callback system: Lua scripts can register
* callback functions via setup_handler(), and C++ code can invoke
* them with event names and optional entity parameters.
*/
class LuaState {
public:
/**
* @brief Construct a new Lua state.
*
* Opens all standard libraries and installs the custom
* library loader for OGRE resource-based Lua modules.
*/
LuaState();
/**
* @brief Destroy the Lua state and close the VM.
*/
~LuaState();
/**
* @brief Get the underlying lua_State pointer.
*/
lua_State *getState() const
{
return L;
}
/**
* @brief Install the custom library loader into package.searchers.
*
* Inserts luaLibraryLoader at position 1 of the searchers table
* so it takes precedence over the default file-system loader.
* This allows Lua's require() to load modules from OGRE resource
* archives.
*/
void installLibraryLoader();
/**
* @brief Late setup: load data.lua and run initialisation.
*
* Opens "data.lua" from the "LuaScripts" resource group and
* executes it. Also runs a simple inline Lua test snippet.
* Called once after the ECS world is fully initialised.
*/
void lateSetup();
/**
* @brief Register a Lua callback function for event handling.
*
* Expects a Lua function on the stack (at index 1).
* Stores a reference to it in the registry so it can be
* called later via callHandler().
*
* @return int Reference index (stored internally).
*/
int setupHandler();
/**
* @brief Call all registered handlers with an event name.
*
* @param event The event name string.
* @return int 0 on success.
*/
int callHandler(const Ogre::String &event);
/**
* @brief Call all registered handlers with an event name and
* two entity IDs.
*
* Entity IDs are mapped through the global idmap (see LuaEntityApi).
*
* @param event The event name string.
* @param e1 First entity (e.g. subject).
* @param e2 Second entity (e.g. object).
* @return int 0 on success.
*/
int callHandler(const Ogre::String &event, flecs::entity e1,
flecs::entity e2);
private:
lua_State *L;
/** Registry references for registered Lua callback functions. */
std::vector<int> setupHandlers;
/**
* @brief Custom Lua loader function for OGRE resource-based modules.
*
* Registered in package.searchers. Translates dots to path
* separators, appends ".lua", and loads the file from the
* "LuaScripts" OGRE resource group.
*
* @param L Lua state.
* @return int 1 (pushes loaded chunk onto stack) or error.
*/
static int luaLibraryLoader(lua_State *L);
};
} // namespace editScene
#endif // EDITSCENE_LUA_STATE_HPP

View File

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

View File

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

View File

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

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

View 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

View File

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

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

View 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

View File

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

View File

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

View File

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

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

View 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

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

View 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

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

View 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