Modules update

This commit is contained in:
2026-04-29 12:53:13 +03:00
parent cca732b41b
commit abe6eef6b3
27 changed files with 2230 additions and 142 deletions

View File

@@ -65,6 +65,14 @@ set(EDITSCENE_SOURCES
systems/PrefabSystem.cpp
ui/PrefabInstanceEditor.cpp
LuaScripting.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 +137,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
@@ -169,6 +180,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 +222,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 +293,7 @@ set(EDITSCENE_HEADERS
gizmo/Gizmo.hpp
gizmo/Cursor3D.hpp
physics/physics.h
LuaScripting.hpp
)
add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS})
@@ -294,6 +315,7 @@ target_link_libraries(editSceneEditor
RecastNavigation::DetourTileCache
RecastNavigation::DetourCrowd
RecastNavigation::DebugUtils
lua
)
target_include_directories(editSceneEditor PRIVATE
@@ -303,6 +325,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,7 +77,10 @@
#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>
@@ -130,6 +135,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 +190,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 +275,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 +382,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 +398,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 +435,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 +459,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)
@@ -605,6 +635,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 +676,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

@@ -29,6 +29,7 @@ class CharacterSystem;
class CellGridSystem;
class RoomLayoutSystem;
class StartupMenuSystem;
class DialogueSystem;
class PlayerControllerSystem;
class BuoyancySystem;
class EditorSunSystem;
@@ -41,6 +42,7 @@ class PathFollowingSystem;
class GoapPlannerSystem;
class ActuatorSystem;
class EventHandlerSystem;
class ItemSystem;
class EditorApp;
/**
@@ -187,6 +189,10 @@ public:
{
return m_startupMenuSystem.get();
}
DialogueSystem *getDialogueSystem() const
{
return m_dialogueSystem.get();
}
ActuatorSystem *getActuatorSystem() const
{
return m_actuatorSystem.get();
@@ -240,10 +246,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

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

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