From abe6eef6b37d97472448fca62827a22eb62318b3 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Wed, 29 Apr 2026 12:53:13 +0300 Subject: [PATCH] Modules update --- src/features/editScene/CMakeLists.txt | 24 ++ src/features/editScene/EditorApp.cpp | 71 +++- src/features/editScene/EditorApp.hpp | 9 +- .../editScene/components/BehaviorTree.hpp | 23 +- .../components/DialogueComponent.hpp | 131 +++++++ .../components/DialogueComponentModule.cpp | 23 ++ .../editScene/components/Inventory.hpp | 165 ++++++++ .../editScene/components/InventoryModule.cpp | 20 + src/features/editScene/components/Item.hpp | 59 +++ .../editScene/components/ItemModule.cpp | 19 + .../editScene/systems/ActuatorSystem.cpp | 224 +++++++---- .../editScene/systems/ActuatorSystem.hpp | 24 +- .../editScene/systems/BehaviorTreeSystem.cpp | 271 ++++++++++++- .../editScene/systems/DialogueSystem.cpp | 224 +++++++++++ .../editScene/systems/DialogueSystem.hpp | 58 +++ .../editScene/systems/EditorUISystem.cpp | 16 + src/features/editScene/systems/ItemSystem.cpp | 364 ++++++++++++++++++ src/features/editScene/systems/ItemSystem.hpp | 88 +++++ .../editScene/systems/SceneSerializer.cpp | 184 +++++++-- .../editScene/systems/SceneSerializer.hpp | 10 +- .../editScene/systems/SmartObjectSystem.hpp | 18 + src/features/editScene/ui/DialogueEditor.cpp | 93 +++++ src/features/editScene/ui/DialogueEditor.hpp | 21 + src/features/editScene/ui/InventoryEditor.cpp | 63 +++ src/features/editScene/ui/InventoryEditor.hpp | 20 + src/features/editScene/ui/ItemEditor.cpp | 130 +++++++ src/features/editScene/ui/ItemEditor.hpp | 20 + 27 files changed, 2230 insertions(+), 142 deletions(-) create mode 100644 src/features/editScene/components/DialogueComponent.hpp create mode 100644 src/features/editScene/components/DialogueComponentModule.cpp create mode 100644 src/features/editScene/components/Inventory.hpp create mode 100644 src/features/editScene/components/InventoryModule.cpp create mode 100644 src/features/editScene/components/Item.hpp create mode 100644 src/features/editScene/components/ItemModule.cpp create mode 100644 src/features/editScene/systems/DialogueSystem.cpp create mode 100644 src/features/editScene/systems/DialogueSystem.hpp create mode 100644 src/features/editScene/systems/ItemSystem.cpp create mode 100644 src/features/editScene/systems/ItemSystem.hpp create mode 100644 src/features/editScene/ui/DialogueEditor.cpp create mode 100644 src/features/editScene/ui/DialogueEditor.hpp create mode 100644 src/features/editScene/ui/InventoryEditor.cpp create mode 100644 src/features/editScene/ui/InventoryEditor.hpp create mode 100644 src/features/editScene/ui/ItemEditor.cpp create mode 100644 src/features/editScene/ui/ItemEditor.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index adf7638..cfd6442 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -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.) diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index a9e05b3..3555766 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -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 #include @@ -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 entitiesToDelete; - m_world.query().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( @@ -362,6 +382,13 @@ void EditorApp::setup() m_eventHandlerSystem = std::make_unique( m_world, m_behaviorTreeSystem.get()); + // Setup Item system + m_itemSystem = std::make_unique( + 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( 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( - m_world); + m_goapPlannerSystem = + std::make_unique(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( m_world, m_sceneMgr, this); + m_dialogueSystem = std::make_unique( + m_world, m_sceneMgr, this); m_playerControllerSystem = std::make_unique( 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(); + m_world.component(); m_world.component(); m_world.component(); @@ -645,6 +676,10 @@ void EditorApp::setupECS() // Register PrefabInstance component m_world.component(); + + // Register Item and Inventory components + m_world.component(); + m_world.component(); } void EditorApp::createDefaultEntities() diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index d587b01..6613b55 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -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 m_goapPlannerSystem; std::unique_ptr m_actuatorSystem; std::unique_ptr m_eventHandlerSystem; + std::unique_ptr m_itemSystem; // Game systems - std::unique_ptr m_startupMenuSystem; + std::unique_ptr m_dialogueSystem; std::unique_ptr m_playerControllerSystem; // State diff --git a/src/features/editScene/components/BehaviorTree.hpp b/src/features/editScene/components/BehaviorTree.hpp index 81251d7..020d812 100644 --- a/src/features/editScene/components/BehaviorTree.hpp +++ b/src/features/editScene/components/BehaviorTree.hpp @@ -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"; } }; diff --git a/src/features/editScene/components/DialogueComponent.hpp b/src/features/editScene/components/DialogueComponent.hpp new file mode 100644 index 0000000..fd0b4d7 --- /dev/null +++ b/src/features/editScene/components/DialogueComponent.hpp @@ -0,0 +1,131 @@ +#ifndef EDITSCENE_DIALOGUE_COMPONENT_HPP +#define EDITSCENE_DIALOGUE_COMPONENT_HPP +#pragma once + +#include +#include +#include +#include + +/** + * 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 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 onChoiceSelected; + + /** Callback invoked when dialogue is dismissed (no choices mode) */ + std::function onDismissed; + + /** Callback invoked when dialogue starts showing */ + std::function onShow; + + /* --- API --- */ + + /** Show dialogue with given text and optional choices */ + void show(const Ogre::String &narrationText, + const std::vector &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 diff --git a/src/features/editScene/components/DialogueComponentModule.cpp b/src/features/editScene/components/DialogueComponentModule.cpp new file mode 100644 index 0000000..f0bc4f6 --- /dev/null +++ b/src/features/editScene/components/DialogueComponentModule.cpp @@ -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_name, DialogueComponent_group, + std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [](flecs::entity e) { + if (e.has()) { + e.remove(); + } + }); +} diff --git a/src/features/editScene/components/Inventory.hpp b/src/features/editScene/components/Inventory.hpp new file mode 100644 index 0000000..2906cc9 --- /dev/null +++ b/src/features/editScene/components/Inventory.hpp @@ -0,0 +1,165 @@ +#ifndef EDITSCENE_INVENTORY_HPP +#define EDITSCENE_INVENTORY_HPP +#pragma once + +#include +#include +#include +#include +#include + +/** + * 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 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 diff --git a/src/features/editScene/components/InventoryModule.cpp b/src/features/editScene/components/InventoryModule.cpp new file mode 100644 index 0000000..f22ad14 --- /dev/null +++ b/src/features/editScene/components/InventoryModule.cpp @@ -0,0 +1,20 @@ +#include "Inventory.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/InventoryEditor.hpp" + +REGISTER_COMPONENT_GROUP("Inventory", "Game", InventoryComponent, + InventoryEditor) +{ + registry.registerComponent( + "Inventory", "Game", std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) + e.set({}); + }, + // Remover + [](flecs::entity e) { + if (e.has()) + e.remove(); + }); +} diff --git a/src/features/editScene/components/Item.hpp b/src/features/editScene/components/Item.hpp new file mode 100644 index 0000000..bcb46fc --- /dev/null +++ b/src/features/editScene/components/Item.hpp @@ -0,0 +1,59 @@ +#ifndef EDITSCENE_ITEM_HPP +#define EDITSCENE_ITEM_HPP +#pragma once + +#include +#include +#include + +/** + * 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 diff --git a/src/features/editScene/components/ItemModule.cpp b/src/features/editScene/components/ItemModule.cpp new file mode 100644 index 0000000..a9b6a41 --- /dev/null +++ b/src/features/editScene/components/ItemModule.cpp @@ -0,0 +1,19 @@ +#include "Item.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/ItemEditor.hpp" + +REGISTER_COMPONENT_GROUP("Item", "Game", ItemComponent, ItemEditor) +{ + registry.registerComponent( + "Item", "Game", std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) + e.set({}); + }, + // Remover + [](flecs::entity e) { + if (e.has()) + e.remove(); + }); +} diff --git a/src/features/editScene/systems/ActuatorSystem.cpp b/src/features/editScene/systems/ActuatorSystem.cpp index 036fc38..29adaa9 100644 --- a/src/features/editScene/systems/ActuatorSystem.cpp +++ b/src/features/editScene/systems/ActuatorSystem.cpp @@ -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()) { - auto &ac = actuator.get_mut(); + flecs::entity actuator = + m_world.entity(m_executingActuatorId); + if (actuator.is_alive() && + actuator.has()) { + auto &ac = + actuator.get_mut(); ac.isExecuting = false; float cooldown = 1.5f; - m_world - .query() - .each([&](flecs::entity, - PlayerControllerComponent &pc) { + m_world.query().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().each( - [&](flecs::entity e, PlayerControllerComponent &pc) { - (void)e; - if (!playerCharacter.is_alive()) { - m_world.query() - .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().each([&](flecs::entity e, + PlayerControllerComponent + &pc) { + (void)e; + if (!playerCharacter.is_alive()) { + m_world.query().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()) return; - Ogre::Vector3 charPos = - playerCharacter.get().node - ->_getDerivedPosition(); + Ogre::Vector3 charPos = playerCharacter.get() + .node->_getDerivedPosition(); // Collect actuators within distance m_visibleActuators.clear(); std::vector inRangeActuators; - m_world.query() - .each([&](flecs::entity e, ActuatorComponent &actuator, - TransformComponent &trans) { + // --- Collect ActuatorComponent entities --- + m_world.query().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().each( + [&](flecs::entity e, ItemComponent &item, + TransformComponent &trans) { + // Skip items that also have an ActuatorComponent + // (those are handled above as actuators) + if (e.has()) + 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()) { - auto &actuator = targetEntity.get(); - 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()) { + auto &actuator = + targetEntity.get(); + 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()) { + // Show "E - Pick up [ItemName]" for items + auto &item = targetEntity.get(); + 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(); - 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()) { + auto &actuator = targetEntity.get(); + 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() && + 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(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); diff --git a/src/features/editScene/systems/ActuatorSystem.hpp b/src/features/editScene/systems/ActuatorSystem.hpp index 6d6d279..148421f 100644 --- a/src/features/editScene/systems/ActuatorSystem.hpp +++ b/src/features/editScene/systems/ActuatorSystem.hpp @@ -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 m_visibleActuators; diff --git a/src/features/editScene/systems/BehaviorTreeSystem.cpp b/src/features/editScene/systems/BehaviorTreeSystem.cpp index 7918db7..8f8ffdd 100644 --- a/src/features/editScene/systems/BehaviorTreeSystem.cpp +++ b/src/features/editScene/systems/BehaviorTreeSystem.cpp @@ -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 #include #include @@ -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()) + return Status::failure; + return itemSystem->hasItem(e, node.name) ? Status::success : + Status::failure; + } + + if (node.type == "hasItemByName") { + if (!itemSystem || !e.has()) + return Status::failure; + return itemSystem->hasItemByName(e, node.name) ? + Status::success : + Status::failure; + } + + if (node.type == "countItem") { + if (!itemSystem || !e.has()) + 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(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()) + 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::max(); + + Ogre::Vector3 charPos = Ogre::Vector3::ZERO; + if (e.has()) { + auto &trans = e.get(); + if (trans.node) + charPos = + trans.node + ->_getDerivedPosition(); + else + charPos = trans.position; + } + + m_world.query().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()) + 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(val); + } + + // Find the item in inventory + int slotIdx = -1; + auto &inv = e.get(); + 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()) { + auto &trans = e.get(); + 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()) + return Status::failure; + + // Find the item in inventory + int slotIdx = -1; + auto &inv = e.get(); + 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()) + 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 parts; + const char *s = node.params.c_str(); + const char *start = s; + while (*s) { + if (*s == ',') { + parts.push_back(Ogre::String( + start, + static_cast( + s - start))); + start = s + 1; + } + s++; + } + if (s > start) + parts.push_back(Ogre::String( + start, static_cast( + 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(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(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; diff --git a/src/features/editScene/systems/DialogueSystem.cpp b/src/features/editScene/systems/DialogueSystem.cpp new file mode 100644 index 0000000..c3e7847 --- /dev/null +++ b/src/features/editScene/systems/DialogueSystem.cpp @@ -0,0 +1,224 @@ +#include "DialogueSystem.hpp" +#include "../EditorApp.hpp" +#include "../components/DialogueComponent.hpp" +#include "../systems/EventBus.hpp" +#include "../components/GoapBlackboard.hpp" +#include +#include +#include +#include +#include + +DialogueSystem::DialogueSystem(flecs::world &world, + Ogre::SceneManager *sceneMgr, + EditorApp *editorApp) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_editorApp(editorApp) +{ + // Subscribe to dialogue events + EventBus::getInstance().subscribe( + "dialogue_show", + [this](const Ogre::String &, const GoapBlackboard ¶ms) { + // Find the first entity with DialogueComponent + m_world.query().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 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().each( + [&](flecs::entity e, DialogueComponent &) { + if (!dialogueEntity.is_alive()) + dialogueEntity = e; + }); + + if (dialogueEntity.is_alive()) { + auto &dc = dialogueEntity.get_mut(); + 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().each( + [&](flecs::entity e, DialogueComponent &) { + if (!dialogueEntity.is_alive()) + dialogueEntity = e; + }); + + if (!dialogueEntity.is_alive()) + return; + + auto &dc = dialogueEntity.get_mut(); + 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(); +} diff --git a/src/features/editScene/systems/DialogueSystem.hpp b/src/features/editScene/systems/DialogueSystem.hpp new file mode 100644 index 0000000..5cacb1b --- /dev/null +++ b/src/features/editScene/systems/DialogueSystem.hpp @@ -0,0 +1,58 @@ +#ifndef EDITSCENE_DIALOGUESYSTEM_HPP +#define EDITSCENE_DIALOGUESYSTEM_HPP +#pragma once + +#include +#include +#include +#include + +#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 diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index c6d9923..e7b7a40 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -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()) { + auto &item = entity.get_mut(); + m_componentRegistry.render(entity, item); + componentCount++; + } + + // Render Inventory if present + if (entity.has()) { + auto &inv = entity.get_mut(); + m_componentRegistry.render(entity, inv); + componentCount++; + } + // Show message if no components if (componentCount == 0) { diff --git a/src/features/editScene/systems/ItemSystem.cpp b/src/features/editScene/systems/ItemSystem.cpp new file mode 100644 index 0000000..d19c5df --- /dev/null +++ b/src/features/editScene/systems/ItemSystem.cpp @@ -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 +#include +#include + +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()) + return false; + + auto &inv = inventoryEntity.get_mut(); + + // 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()) + return false; + if (!itemEntity.is_alive() || !itemEntity.has()) + return false; + + auto &item = itemEntity.get_mut(); + + // 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()) { + auto &trans = itemEntity.get_mut(); + 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()) + return 0; + + auto &inv = inventoryEntity.get_mut(); + 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()) + return false; + + auto &inv = inventoryEntity.get_mut(); + 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()) + return false; + if (!toInventory.is_alive() || !toInventory.has()) + return false; + + auto &fromInv = fromInventory.get_mut(); + 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()) + return false; + + return inventoryEntity.get().hasItem(itemId); +} + +bool ItemSystem::hasItemByName(flecs::entity inventoryEntity, + const Ogre::String &itemName) const +{ + if (!inventoryEntity.is_alive() || + !inventoryEntity.has()) + return false; + + return inventoryEntity.get().hasItemByName( + itemName); +} + +int ItemSystem::countItem(flecs::entity inventoryEntity, + const Ogre::String &itemId) const +{ + if (!inventoryEntity.is_alive() || + !inventoryEntity.has()) + return 0; + + return inventoryEntity.get().countItem(itemId); +} + +bool ItemSystem::dropItem(flecs::entity inventoryEntity, int slotIndex, + const Ogre::Vector3 &worldPosition, int count) +{ + if (!inventoryEntity.is_alive() || + !inventoryEntity.has()) + return false; + + auto &inv = inventoryEntity.get_mut(); + 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()) { + auto &item = itemEntity.get_mut(); + item.stackSize = dropCount; + + if (itemEntity.has()) { + auto &trans = + itemEntity.get_mut(); + 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(slot.itemName, slot.itemType)); + auto &item = itemEntity.get_mut(); + 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(trans); + + EntityNameComponent nameComp; + nameComp.name = slot.itemName + "_dropped"; + itemEntity.set(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()) + return false; + + auto &inv = inventoryEntity.get(); + 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().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()) + return false; + if (!itemEntity.is_alive() || !itemEntity.has()) + return false; + + bool result = addItemEntityToInventory(characterEntity, itemEntity); + if (result) { + Ogre::LogManager::getSingleton().logMessage( + "[ItemSystem] Picked up: " + + itemEntity.get().itemName); + } + return result; +} diff --git a/src/features/editScene/systems/ItemSystem.hpp b/src/features/editScene/systems/ItemSystem.hpp new file mode 100644 index 0000000..6a63aeb --- /dev/null +++ b/src/features/editScene/systems/ItemSystem.hpp @@ -0,0 +1,88 @@ +#ifndef EDITSCENE_ITEM_SYSTEM_HPP +#define EDITSCENE_ITEM_SYSTEM_HPP +#pragma once + +#include +#include +#include + +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 diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index ae234b3..943ea7d 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -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()) { + json["item"] = serializeItem(entity); + } + + if (entity.has()) { + json["inventory"] = serializeInventory(entity); + } + if (entity.has()) { 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(); + const GoapPlannerComponent &planner = + entity.get(); nlohmann::json json; json["actionNames"] = planner.actionNames; if (!planner.goalNames.empty()) @@ -3684,7 +3709,6 @@ void SceneSerializer::deserializeNavMeshGeometrySource( entity.set(src); } - nlohmann::json SceneSerializer::serializeActuator(flecs::entity entity) { const ActuatorComponent &actuator = entity.get(); @@ -3711,10 +3735,10 @@ void SceneSerializer::deserializeActuator(flecs::entity entity, entity.set(actuator); } - nlohmann::json SceneSerializer::serializeEventHandler(flecs::entity entity) { - const EventHandlerComponent &handler = entity.get(); + const EventHandlerComponent &handler = + entity.get(); 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(handler); } + +nlohmann::json SceneSerializer::serializeItem(flecs::entity entity) +{ + const ItemComponent &item = entity.get(); + 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(); + 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(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(inv); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index ea52a97..96d106c 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -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); diff --git a/src/features/editScene/systems/SmartObjectSystem.hpp b/src/features/editScene/systems/SmartObjectSystem.hpp index e48fb3e..5a1c8b2 100644 --- a/src/features/editScene/systems/SmartObjectSystem.hpp +++ b/src/features/editScene/systems/SmartObjectSystem.hpp @@ -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 m_states; diff --git a/src/features/editScene/ui/DialogueEditor.cpp b/src/features/editScene/ui/DialogueEditor.cpp new file mode 100644 index 0000000..c85b60b --- /dev/null +++ b/src/features/editScene/ui/DialogueEditor.cpp @@ -0,0 +1,93 @@ +#include "DialogueEditor.hpp" +#include + +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 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; +} diff --git a/src/features/editScene/ui/DialogueEditor.hpp b/src/features/editScene/ui/DialogueEditor.hpp new file mode 100644 index 0000000..3da9215 --- /dev/null +++ b/src/features/editScene/ui/DialogueEditor.hpp @@ -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 { +public: + bool renderComponent(flecs::entity entity, + DialogueComponent &dc) override; + const char *getName() const override + { + return "Dialogue Box"; + } +}; + +#endif // EDITSCENE_DIALOGUEEDITOR_HPP diff --git a/src/features/editScene/ui/InventoryEditor.cpp b/src/features/editScene/ui/InventoryEditor.cpp new file mode 100644 index 0000000..bbc8ca2 --- /dev/null +++ b/src/features/editScene/ui/InventoryEditor.cpp @@ -0,0 +1,63 @@ +#include "InventoryEditor.hpp" +#include + +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; +} diff --git a/src/features/editScene/ui/InventoryEditor.hpp b/src/features/editScene/ui/InventoryEditor.hpp new file mode 100644 index 0000000..27b34b6 --- /dev/null +++ b/src/features/editScene/ui/InventoryEditor.hpp @@ -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 { +public: + const char *getName() const override + { + return "Inventory"; + } + +protected: + bool renderComponent(flecs::entity entity, + InventoryComponent &inventory) override; +}; + +#endif // EDITSCENE_INVENTORY_EDITOR_HPP diff --git a/src/features/editScene/ui/ItemEditor.cpp b/src/features/editScene/ui/ItemEditor.cpp new file mode 100644 index 0000000..a8527db --- /dev/null +++ b/src/features/editScene/ui/ItemEditor.cpp @@ -0,0 +1,130 @@ +#include "ItemEditor.hpp" +#include "../components/ActionDatabase.hpp" +#include + +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().each( + [&](flecs::entity, ActionDatabase &database) { + if (!db) + db = &database; + }); + + if (db && !db->actions.empty()) { + static int selectedAction = -1; + std::vector availableNames; + std::vector 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; +} diff --git a/src/features/editScene/ui/ItemEditor.hpp b/src/features/editScene/ui/ItemEditor.hpp new file mode 100644 index 0000000..9e219b0 --- /dev/null +++ b/src/features/editScene/ui/ItemEditor.hpp @@ -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 { +public: + const char *getName() const override + { + return "Item"; + } + +protected: + bool renderComponent(flecs::entity entity, + ItemComponent &item) override; +}; + +#endif // EDITSCENE_ITEM_EDITOR_HPP