From 5bb20d416d146fd12ba5127b8a2b5260aec75b29 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Thu, 14 May 2026 02:28:33 +0300 Subject: [PATCH] Item registry --- src/features/editScene/CMakeLists.txt | 22 + src/features/editScene/EditorApp.cpp | 8 + .../editScene/components/Inventory.hpp | 60 +-- src/features/editScene/components/Item.hpp | 44 +- src/features/editScene/docs/ItemApi.md | 282 +++++++++++++ .../editScene/lua/LuaComponentApi.cpp | 42 +- src/features/editScene/lua/LuaItemApi.cpp | 392 ++++++++++++++++++ src/features/editScene/lua/LuaItemApi.hpp | 14 + .../editScene/systems/ActuatorSystem.cpp | 7 +- .../editScene/systems/BehaviorTreeSystem.cpp | 74 +--- .../systems/ContainerStateRegistry.cpp | 125 ++++++ .../systems/ContainerStateRegistry.hpp | 61 +++ .../editScene/systems/EditorUISystem.cpp | 9 + .../editScene/systems/EditorUISystem.hpp | 3 + .../editScene/systems/ItemRegistry.cpp | 371 +++++++++++++++++ .../editScene/systems/ItemRegistry.hpp | 137 ++++++ src/features/editScene/systems/ItemSystem.cpp | 113 ++--- src/features/editScene/systems/ItemSystem.hpp | 23 +- .../editScene/systems/SceneSerializer.cpp | 78 ++-- .../editScene/tests/component_lua_test.cpp | 18 +- .../editScene/tests/item_lua_test.cpp | 373 +++++++++++++++++ .../editScene/tests/lua_test_stubs.cpp | 346 ++++++++++++++++ src/features/editScene/ui/InventoryEditor.cpp | 24 +- src/features/editScene/ui/ItemEditor.cpp | 142 +++---- 24 files changed, 2396 insertions(+), 372 deletions(-) create mode 100644 src/features/editScene/docs/ItemApi.md create mode 100644 src/features/editScene/lua/LuaItemApi.cpp create mode 100644 src/features/editScene/lua/LuaItemApi.hpp create mode 100644 src/features/editScene/systems/ContainerStateRegistry.cpp create mode 100644 src/features/editScene/systems/ContainerStateRegistry.hpp create mode 100644 src/features/editScene/systems/ItemRegistry.cpp create mode 100644 src/features/editScene/systems/ItemRegistry.hpp create mode 100644 src/features/editScene/tests/item_lua_test.cpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index ac671c8..39ee009 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -36,6 +36,8 @@ set(EDITSCENE_SOURCES systems/FurnitureLibrary.cpp systems/StartupMenuSystem.cpp systems/PauseMenuSystem.cpp + systems/ItemRegistry.cpp + systems/ContainerStateRegistry.cpp systems/PlayerControllerSystem.cpp systems/CharacterSlotSystem.cpp systems/CharacterRegistry.cpp @@ -144,6 +146,7 @@ set(EDITSCENE_SOURCES components/PlayerControllerModule.cpp systems/DialogueSystem.cpp lua/LuaDialogueApi.cpp + lua/LuaItemApi.cpp components/Formula.cpp components/CharacterClassDatabase.cpp systems/CharacterClassSystem.cpp @@ -200,6 +203,7 @@ set(EDITSCENE_HEADERS components/PlayerController.hpp systems/DialogueSystem.hpp lua/LuaDialogueApi.hpp + lua/LuaItemApi.hpp components/Formula.hpp components/CharacterClassDatabase.hpp @@ -207,6 +211,8 @@ set(EDITSCENE_HEADERS systems/CharacterClassSystem.hpp systems/StartupMenuSystem.hpp systems/PauseMenuSystem.hpp + systems/ItemRegistry.hpp + systems/ContainerStateRegistry.hpp systems/PlayerControllerSystem.hpp systems/EditorUISystem.hpp systems/CellGridSystem.hpp @@ -495,6 +501,22 @@ target_include_directories(game_mode_lua_test PRIVATE ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src ) +# Test: Item / Inventory / Container Lua API +add_executable(item_lua_test + tests/item_lua_test.cpp + tests/lua_test_stubs.cpp +) + +target_link_libraries(item_lua_test + lua +) + +target_include_directories(item_lua_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/tests + ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src +) + # Test: Character Lua API add_executable(character_lua_test tests/character_lua_test.cpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index ebaee46..ffd8d50 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -31,6 +31,8 @@ #include "systems/StartupMenuSystem.hpp" #include "systems/PauseMenuSystem.hpp" #include "systems/DialogueSystem.hpp" +#include "systems/ItemRegistry.hpp" +#include "systems/ContainerStateRegistry.hpp" #include "systems/CharacterClassSystem.hpp" #include "systems/PregnancySystem.hpp" #include "components/CharacterClassDatabase.hpp" @@ -100,6 +102,7 @@ #include "lua/LuaGameModeApi.hpp" #include "lua/LuaCharacterClassApi.hpp" #include "lua/LuaDialogueApi.hpp" +#include "lua/LuaItemApi.hpp" //============================================================================= // ImGuiRenderListener Implementation @@ -464,6 +467,10 @@ void EditorApp::setup() DialogueSystem::getInstance().init(this); DialogueSystem::getInstance().loadSettings("dialogue.json"); PauseMenuSystem::getInstance().init(this); + static ItemRegistry s_itemRegistry; + ItemRegistry::getSingleton().initialize(); + ContainerStateRegistry::getInstance().loadFromFile( + "container_state.json"); m_characterClassSystem = std::make_unique( @@ -548,6 +555,7 @@ void EditorApp::setup() editScene::registerLuaCharacterClassApi(L); editScene::registerLuaCharacterApi(L); editScene::registerLuaDialogueApi(L); + editScene::registerLuaItemApi(L); // Run late setup: load data.lua and initial scripts. m_lua.lateSetup(); diff --git a/src/features/editScene/components/Inventory.hpp b/src/features/editScene/components/Inventory.hpp index 2906cc9..c7d47a1 100644 --- a/src/features/editScene/components/Inventory.hpp +++ b/src/features/editScene/components/Inventory.hpp @@ -8,41 +8,33 @@ #include #include +#include "../systems/ItemRegistry.hpp" + /** * 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 ID of the item (0 if slot is empty or no world entity) 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; + // Item registry key + std::string itemId; + + // Stack size int stackSize = 0; - int maxStackSize = 99; - float weight = 0.1f; - int value = 1; - Ogre::String useActionName; bool isEmpty() const { - return itemEntity == 0 && stackSize <= 0; + return itemId.empty() && itemEntity == 0; } void clear() { itemEntity = 0; itemId.clear(); - itemName.clear(); - itemType.clear(); stackSize = 0; - maxStackSize = 99; - weight = 0.1f; - value = 1; - useActionName.clear(); } }; @@ -52,9 +44,6 @@ struct InventorySlot { * 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 @@ -70,12 +59,14 @@ struct InventoryComponent { 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; + // Persistent ID for scene containers (empty for character inventories) + std::string containerId; + InventoryComponent() = default; explicit InventoryComponent(int maxSlots_) @@ -97,7 +88,7 @@ struct InventoryComponent { } /** Find a slot containing an item with the given itemId. */ - int findItem(const Ogre::String &itemId) const + int findItem(const std::string &itemId) const { for (int i = 0; i < (int)slots.size(); i++) { if (!slots[i].isEmpty() && slots[i].itemId == itemId) @@ -106,17 +97,6 @@ struct InventoryComponent { 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 { @@ -129,7 +109,7 @@ struct InventoryComponent { } /** Count how many of a specific itemId are in the inventory. */ - int countItem(const Ogre::String &itemId) const + int countItem(const std::string &itemId) const { int count = 0; for (const auto &slot : slots) { @@ -140,24 +120,20 @@ struct InventoryComponent { } /** Check if inventory has at least one of a specific itemId. */ - bool hasItem(const Ogre::String &itemId) const + bool hasItem(const std::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. */ + /** Recalculate total weight from registry. */ void recalculateWeight() { totalWeight = 0.0f; for (const auto &slot : slots) { if (!slot.isEmpty()) - totalWeight += slot.weight * slot.stackSize; + totalWeight += + ItemRegistry::getSingleton().getWeight(slot.itemId) * + slot.stackSize; } } }; diff --git a/src/features/editScene/components/Item.hpp b/src/features/editScene/components/Item.hpp index bcb46fc..1343527 100644 --- a/src/features/editScene/components/Item.hpp +++ b/src/features/editScene/components/Item.hpp @@ -4,54 +4,26 @@ #include #include -#include /** - * Item definition component. + * Item reference 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. + * All item properties (name, type, weight, etc.) are stored in + * the ItemRegistry singleton and looked up by itemId. */ 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) + // Registry key for this item definition Ogre::String itemId; - // Stack size: how many of this item are in this stack + // Stack size for this world entity 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) + explicit ItemComponent(const Ogre::String &id, int stack = 1) + : itemId(id) + , stackSize(stack) { } }; diff --git a/src/features/editScene/docs/ItemApi.md b/src/features/editScene/docs/ItemApi.md new file mode 100644 index 0000000..09720bc --- /dev/null +++ b/src/features/editScene/docs/ItemApi.md @@ -0,0 +1,282 @@ +# Item, Inventory & Container Lua API + +This document describes the Lua APIs for managing item definitions, inventories, +and persistent container state in the editScene editor. + +--- + +## Overview + +The item system is split into three namespaces: + +| Namespace | Purpose | +|-----------|---------| +| `ecs.items` | Register and query item **definitions** (global registry) | +| `ecs.inventory` | Add, remove, and query items in an entity's **inventory** | +| `ecs.container` | Save/load persistent **container state** by `containerId` | + +Item data lives in a single authoritative registry (`ItemRegistry`). Entities +only store `itemId` + `stackSize`, so changing a definition (name, weight, etc.) +propagates automatically to all instances. + +--- + +## `ecs.items` — Item Definition Registry + +### `ecs.items.register(itemId, definition)` + +Register a new item type or overwrite an existing one. + +**Parameters:** +- `itemId` *(string)* — Unique identifier, e.g. `"potion_health"` +- `definition` *(table)* — Item properties + +**Definition fields:** + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `itemName` | string | `""` | Human-readable name | +| `itemType` | string | `"misc"` | Category: `consumable`, `weapon`, `armor`, `misc`, … | +| `maxStackSize` | int | `99` | How many fit in one inventory slot | +| `weight` | float | `0.1` | Weight per unit (for encumbrance) | +| `value` | int | `1` | Base trade value | +| `useActionName` | string | `""` | Action triggered on "use" | +| `unique` | bool | `false` | If `true`, only one may exist in the world | + +**Example:** +```lua +ecs.items.register('potion_health', { + itemName = 'Health Potion', + itemType = 'consumable', + maxStackSize = 10, + weight = 0.5, + value = 25, + useActionName = 'drink_potion', + unique = false +}) +``` + +--- + +### `ecs.items.find(itemId)` → table | nil + +Look up a registered item definition. + +**Returns:** +- Table with all definition fields (see above), or `nil` if not found. + +**Example:** +```lua +local def = ecs.items.find('potion_health') +if def then + print(def.itemName, 'weight:', def.weight) +end +``` + +--- + +### `ecs.items.list()` → array of strings + +Get a list of all registered `itemId`s. + +**Example:** +```lua +local allItems = ecs.items.list() +for _, id in ipairs(allItems) do + print(id) +end +``` + +--- + +### `ecs.items.is_unique(itemId)` → bool + +Check whether an item is marked as unique. + +**Example:** +```lua +if ecs.items.is_unique('amulet_legendary') then + print('There can be only one!') +end +``` + +--- + +## `ecs.inventory` — Entity Inventory Operations + +All functions operate on a specific entity that has an `InventoryComponent`. +The entity is referenced by its numeric ECS id (the same id used with +`ecs.create_entity()`). + +### `ecs.inventory.add(entityId, itemId, count)` → bool + +Add `count` copies of an item to the entity's inventory. + +**Parameters:** +- `entityId` *(number)* — ECS entity id +- `itemId` *(string)* — Registered item id +- `count` *(number)* — Quantity to add (default 1 if omitted in C++, but Lua + wrapper requires all 3 arguments) + +**Returns:** `true` if the operation succeeded, `false` if the inventory is +full, over weight limit, or the item is unique and already exists elsewhere. + +**Example:** +```lua +local player = ecs.get_player_entity() -- or any entity id +ecs.inventory.add(player, 'potion_health', 5) +``` + +--- + +### `ecs.inventory.remove(entityId, itemId, count)` → int + +Remove up to `count` copies of an item. + +**Returns:** The number of items actually removed. + +**Example:** +```lua +local removed = ecs.inventory.remove(player, 'potion_health', 2) +print('Removed', removed, 'potions') +``` + +--- + +### `ecs.inventory.has(entityId, itemId)` → bool + +Check if the inventory contains at least one of the given item. + +**Example:** +```lua +if ecs.inventory.has(player, 'key_temple') then + ecs.event('unlock_temple_door') +end +``` + +--- + +### `ecs.inventory.count(entityId, itemId)` → int + +Get the total stack size of an item across all slots. + +**Example:** +```lua +local coins = ecs.inventory.count(player, 'gold_coin') +print('You have', coins, 'gold coins') +``` + +--- + +### `ecs.inventory.get_slots(entityId)` → array of slots + +Get a snapshot of the inventory's non-empty slots. + +**Returns:** Array of tables, each with: +- `itemId` *(string)* +- `stackSize` *(number)* + +**Example:** +```lua +local slots = ecs.inventory.get_slots(player) +for _, slot in ipairs(slots) do + print(slot.itemId, 'x' .. slot.stackSize) +end +``` + +--- + +### `ecs.inventory.set_slots(entityId, slots)` + +Overwrite the entire inventory with a new array of slots. + +**Parameters:** +- `slots` *(array)* — Each element is `{ itemId = "...", stackSize = N }` + +**Example:** +```lua +ecs.inventory.set_slots(chestEntity, { + { itemId = 'gold_coin', stackSize = 50 }, + { itemId = 'iron_dagger', stackSize = 1 } +}) +``` + +--- + +## `ecs.container` — Persistent Container State + +Containers with a `containerId` in their `InventoryComponent` automatically +sync to the global `ContainerStateRegistry`. This lets chests, shops, and +loot crates retain their contents across scene reloads. + +The `ecs.container` API exposes the same persistence layer directly so Lua +scripts can read or override container state. + +### `ecs.container.get_state(containerId)` → array of slots + +Load the persisted contents of a container. + +**Returns:** Array of `{ itemId, stackSize }` tables. Empty array if no state +has been saved. + +**Example:** +```lua +local loot = ecs.container.get_state('chest_village') +for _, slot in ipairs(loot) do + print('Chest contains', slot.itemId, 'x' .. slot.stackSize) +end +``` + +--- + +### `ecs.container.set_state(containerId, slots)` + +Overwrite the persisted state of a container. + +**Parameters:** +- `slots` *(array)* — `{ itemId = "...", stackSize = N }` + +**Example:** +```lua +ecs.container.set_state('chest_village', { + { itemId = 'healing_herb', stackSize = 3 }, + { itemId = 'rusty_key', stackSize = 1 } +}) +``` + +--- + +### `ecs.container.clear_state(containerId)` + +Delete the persisted state for a container, so it will revert to its scene + defaults on next load. + +**Example:** +```lua +ecs.container.clear_state('chest_village') +``` + +--- + +## Persistence + +| File | Content | Auto-save? | +|------|---------|------------| +| `items.json` | All item definitions | Yes, on every mutation | +| `container_state.json` | Container slot overrides | Yes, on every mutation | + +Both files live in the working directory of the editor executable and are +loaded automatically on startup. + +--- + +## Unique Items + +When `unique = true` is set on an item definition, the engine rejects any +attempt to create a duplicate: + +- `ecs.inventory.add` returns `false` if the item already exists in any + inventory or as a world entity. +- Scene deserialization skips unique items that are already present. + +This is useful for quest keys, legendary artifacts, and one-off rewards. diff --git a/src/features/editScene/lua/LuaComponentApi.cpp b/src/features/editScene/lua/LuaComponentApi.cpp index 944f791..a0f3148 100644 --- a/src/features/editScene/lua/LuaComponentApi.cpp +++ b/src/features/editScene/lua/LuaComponentApi.cpp @@ -829,42 +829,15 @@ static void registerAllComponents() // --- Item --- REGISTER_COMPONENT( - ItemComponent, "Item", lua_pushstring(L, c.itemName.c_str()); - lua_setfield(L, -2, "itemName"); - lua_pushstring(L, c.itemType.c_str()); - lua_setfield(L, -2, "itemType"); - lua_pushstring(L, c.itemId.c_str()); - lua_setfield(L, -2, "itemId"); lua_pushinteger(L, c.stackSize); + ItemComponent, "Item", lua_pushstring(L, c.itemId.c_str()); + lua_setfield(L, -2, "itemId"); + lua_pushinteger(L, c.stackSize); lua_setfield(L, -2, "stackSize"); - lua_pushinteger(L, c.maxStackSize); - lua_setfield(L, -2, "maxStackSize"); - lua_pushnumber(L, c.weight); lua_setfield(L, -2, "weight"); - lua_pushinteger(L, c.value); lua_setfield(L, -2, "value"); - lua_pushstring(L, c.useActionName.c_str()); - lua_setfield(L, -2, "useActionName"); - , if (lua_getfield(L, idx, "itemName"), lua_isstring(L, -1)) - c.itemName = lua_tostring(L, -1); - lua_pop(L, 1); - if (lua_getfield(L, idx, "itemType"), lua_isstring(L, -1)) - c.itemType = lua_tostring(L, -1); - lua_pop(L, 1); - if (lua_getfield(L, idx, "itemId"), lua_isstring(L, -1)) - c.itemId = lua_tostring(L, -1); + , if (lua_getfield(L, idx, "itemId"), lua_isstring(L, -1)) + c.itemId = lua_tostring(L, -1); lua_pop(L, 1); if (lua_getfield(L, idx, "stackSize"), lua_isnumber(L, -1)) c.stackSize = (int)lua_tointeger(L, -1); - lua_pop(L, 1); - if (lua_getfield(L, idx, "maxStackSize"), lua_isnumber(L, -1)) - c.maxStackSize = (int)lua_tointeger(L, -1); - lua_pop(L, 1); - if (lua_getfield(L, idx, "weight"), lua_isnumber(L, -1)) - c.weight = (float)lua_tonumber(L, -1); - lua_pop(L, 1); - if (lua_getfield(L, idx, "value"), lua_isnumber(L, -1)) - c.value = (int)lua_tointeger(L, -1); - lua_pop(L, 1); - if (lua_getfield(L, idx, "useActionName"), lua_isstring(L, -1)) - c.useActionName = lua_tostring(L, -1); lua_pop(L, 1);); // --- Inventory --- @@ -874,6 +847,8 @@ static void registerAllComponents() lua_setfield(L, -2, "maxWeight"); lua_pushboolean(L, c.isContainer ? 1 : 0); lua_setfield(L, -2, "isContainer"); + lua_pushstring(L, c.containerId.c_str()); + lua_setfield(L, -2, "containerId"); , if (lua_getfield(L, idx, "maxSlots"), lua_isnumber(L, -1)) c.maxSlots = (int)lua_tointeger(L, -1); lua_pop(L, 1); @@ -882,6 +857,9 @@ static void registerAllComponents() lua_pop(L, 1); if (lua_getfield(L, idx, "isContainer"), lua_isboolean(L, -1)) c.isContainer = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); + if (lua_getfield(L, idx, "containerId"), lua_isstring(L, -1)) + c.containerId = lua_tostring(L, -1); lua_pop(L, 1);); // --- Lod --- diff --git a/src/features/editScene/lua/LuaItemApi.cpp b/src/features/editScene/lua/LuaItemApi.cpp new file mode 100644 index 0000000..2e3f5de --- /dev/null +++ b/src/features/editScene/lua/LuaItemApi.cpp @@ -0,0 +1,392 @@ +#include "LuaItemApi.hpp" +#include "../systems/ItemRegistry.hpp" +#include "../systems/ContainerStateRegistry.hpp" +#include "../systems/ItemSystem.hpp" +#include "../components/Item.hpp" +#include "../components/Inventory.hpp" +#include +#include + +static flecs::world getWorld(lua_State *L) +{ + lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld"); + OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered"); + flecs::world *world = + static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + return *world; +} + +namespace editScene +{ + +// --------------------------------------------------------------------------- +// ecs.items API +// --------------------------------------------------------------------------- + +static int luaItemRegister(lua_State *L) +{ + if (lua_gettop(L) < 2 || !lua_isstring(L, 1) || !lua_istable(L, 2)) + return 0; + + std::string itemId = lua_tostring(L, 1); + ItemRegistry::ItemDefinition def; + def.itemId = itemId; + + if (lua_getfield(L, 2, "itemName"), lua_isstring(L, -1)) + def.itemName = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "itemType"), lua_isstring(L, -1)) + def.itemType = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "maxStackSize"), lua_isnumber(L, -1)) + def.maxStackSize = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "weight"), lua_isnumber(L, -1)) + def.weight = (float)lua_tonumber(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "value"), lua_isnumber(L, -1)) + def.value = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "useActionName"), lua_isstring(L, -1)) + def.useActionName = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "unique"), lua_isboolean(L, -1)) + def.unique = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); + + ItemRegistry::getSingleton().registerItem(def); + return 0; +} + +static int luaItemFind(lua_State *L) +{ + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + + std::string itemId = lua_tostring(L, 1); + auto *def = ItemRegistry::getSingleton().findDefinition(itemId); + if (!def) + return 0; + + lua_newtable(L); + lua_pushstring(L, def->itemId.c_str()); + lua_setfield(L, -2, "itemId"); + lua_pushstring(L, def->itemName.c_str()); + lua_setfield(L, -2, "itemName"); + lua_pushstring(L, def->itemType.c_str()); + lua_setfield(L, -2, "itemType"); + lua_pushinteger(L, def->maxStackSize); + lua_setfield(L, -2, "maxStackSize"); + lua_pushnumber(L, def->weight); + lua_setfield(L, -2, "weight"); + lua_pushinteger(L, def->value); + lua_setfield(L, -2, "value"); + lua_pushstring(L, def->useActionName.c_str()); + lua_setfield(L, -2, "useActionName"); + lua_pushboolean(L, def->unique ? 1 : 0); + lua_setfield(L, -2, "unique"); + return 1; +} + +static int luaItemList(lua_State *L) +{ + lua_newtable(L); + int idx = 1; + for (const auto &pair : ItemRegistry::getSingleton().getDefinitions()) { + lua_pushstring(L, pair.second.itemId.c_str()); + lua_rawseti(L, -2, idx++); + } + return 1; +} + +static int luaItemIsUnique(lua_State *L) +{ + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + lua_pushboolean(L, + ItemRegistry::getSingleton().isUnique(lua_tostring(L, 1)) ? + 1 : + 0); + return 1; +} + +// --------------------------------------------------------------------------- +// ecs.inventory API +// --------------------------------------------------------------------------- + +static flecs::entity luaCheckEntity(lua_State *L, int idx) +{ + if (!lua_isnumber(L, idx)) + return flecs::entity::null(); + flecs::world world = getWorld(L); + return world.entity((flecs::entity_t)lua_tointeger(L, idx)); +} + +static int luaInventoryAdd(lua_State *L) +{ + if (lua_gettop(L) < 3 || !lua_isnumber(L, 1) || !lua_isstring(L, 2) || + !lua_isnumber(L, 3)) + return 0; + + flecs::entity e = luaCheckEntity(L, 1); + std::string itemId = lua_tostring(L, 2); + int count = (int)lua_tointeger(L, 3); + + // Find ItemSystem via ECS world query + bool added = false; + if (e.is_alive() && e.has()) { + auto &inv = e.get_mut(); + int maxStack = ItemRegistry::getSingleton().getMaxStackSize(itemId); + // Simple add logic (no ItemSystem pointer available in Lua) + while (count > 0) { + bool stacked = false; + for (auto &slot : inv.slots) { + if (!slot.isEmpty() && slot.itemId == itemId && + slot.stackSize < maxStack) { + int space = maxStack - slot.stackSize; + int add = std::min(space, count); + slot.stackSize += add; + count -= add; + stacked = true; + if (count <= 0) + break; + } + } + if (!stacked || count > 0) { + int slotIdx = inv.findEmptySlot(); + if (slotIdx < 0) + break; + while ((int)inv.slots.size() <= slotIdx) + inv.slots.emplace_back(); + auto &slot = inv.slots[slotIdx]; + slot.itemId = itemId; + int add = std::min(count, maxStack); + slot.stackSize = add; + count -= add; + } + } + inv.recalculateWeight(); + added = true; + } + lua_pushboolean(L, added ? 1 : 0); + return 1; +} + +static int luaInventoryRemove(lua_State *L) +{ + if (lua_gettop(L) < 3 || !lua_isnumber(L, 1) || !lua_isstring(L, 2) || + !lua_isnumber(L, 3)) + return 0; + + flecs::entity e = luaCheckEntity(L, 1); + std::string itemId = lua_tostring(L, 2); + int count = (int)lua_tointeger(L, 3); + int removed = 0; + + if (e.is_alive() && e.has()) { + auto &inv = e.get_mut(); + 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 rem = std::min(count, slot.stackSize); + slot.stackSize -= rem; + count -= rem; + removed += rem; + if (slot.stackSize <= 0) + slot.clear(); + } + inv.recalculateWeight(); + } + lua_pushinteger(L, removed); + return 1; +} + +static int luaInventoryHas(lua_State *L) +{ + if (lua_gettop(L) < 2 || !lua_isnumber(L, 1) || !lua_isstring(L, 2)) + return 0; + + flecs::entity e = luaCheckEntity(L, 1); + std::string itemId = lua_tostring(L, 2); + bool has = false; + if (e.is_alive() && e.has()) + has = e.get().hasItem(itemId); + lua_pushboolean(L, has ? 1 : 0); + return 1; +} + +static int luaInventoryCount(lua_State *L) +{ + if (lua_gettop(L) < 2 || !lua_isnumber(L, 1) || !lua_isstring(L, 2)) + return 0; + + flecs::entity e = luaCheckEntity(L, 1); + std::string itemId = lua_tostring(L, 2); + int count = 0; + if (e.is_alive() && e.has()) + count = e.get().countItem(itemId); + lua_pushinteger(L, count); + return 1; +} + +static int luaInventoryGetSlots(lua_State *L) +{ + if (lua_gettop(L) < 1 || !lua_isnumber(L, 1)) + return 0; + + flecs::entity e = luaCheckEntity(L, 1); + lua_newtable(L); + if (e.is_alive() && e.has()) { + auto &inv = e.get(); + int idx = 1; + for (const auto &slot : inv.slots) { + if (slot.isEmpty()) + continue; + lua_newtable(L); + lua_pushstring(L, slot.itemId.c_str()); + lua_setfield(L, -2, "itemId"); + lua_pushinteger(L, slot.stackSize); + lua_setfield(L, -2, "stackSize"); + lua_rawseti(L, -2, idx++); + } + } + return 1; +} + +static int luaInventorySetSlots(lua_State *L) +{ + if (lua_gettop(L) < 2 || !lua_isnumber(L, 1) || !lua_istable(L, 2)) + return 0; + + flecs::entity e = luaCheckEntity(L, 1); + if (!e.is_alive() || !e.has()) + return 0; + + auto &inv = e.get_mut(); + inv.slots.clear(); + int len = (int)lua_rawlen(L, 2); + for (int i = 1; i <= len; i++) { + lua_rawgeti(L, 2, i); + if (lua_istable(L, -1)) { + InventorySlot slot; + if (lua_getfield(L, -1, "itemId"), lua_isstring(L, -1)) + slot.itemId = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, -1, "stackSize"), lua_isnumber(L, -1)) + slot.stackSize = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + if (!slot.isEmpty()) + inv.slots.push_back(slot); + } + lua_pop(L, 1); + } + inv.recalculateWeight(); + return 0; +} + +// --------------------------------------------------------------------------- +// ecs.container API +// --------------------------------------------------------------------------- + +static int luaContainerGetState(lua_State *L) +{ + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + std::string containerId = lua_tostring(L, 1); + auto slots = ContainerStateRegistry::getInstance().getState( + containerId); + lua_newtable(L); + int idx = 1; + for (const auto &slot : slots) { + lua_newtable(L); + lua_pushstring(L, slot.itemId.c_str()); + lua_setfield(L, -2, "itemId"); + lua_pushinteger(L, slot.stackSize); + lua_setfield(L, -2, "stackSize"); + lua_rawseti(L, -2, idx++); + } + return 1; +} + +static int luaContainerSetState(lua_State *L) +{ + if (lua_gettop(L) < 2 || !lua_isstring(L, 1) || !lua_istable(L, 2)) + return 0; + std::string containerId = lua_tostring(L, 1); + std::vector slots; + int len = (int)lua_rawlen(L, 2); + for (int i = 1; i <= len; i++) { + lua_rawgeti(L, 2, i); + if (lua_istable(L, -1)) { + ContainerStateRegistry::ContainerSlot slot; + if (lua_getfield(L, -1, "itemId"), lua_isstring(L, -1)) + slot.itemId = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, -1, "stackSize"), lua_isnumber(L, -1)) + slot.stackSize = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + slots.push_back(slot); + } + lua_pop(L, 1); + } + ContainerStateRegistry::getInstance().loadState(containerId, slots); + return 0; +} + +static int luaContainerClearState(lua_State *L) +{ + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + ContainerStateRegistry::getInstance().clearState(lua_tostring(L, 1)); + return 0; +} + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +void registerLuaItemApi(lua_State *L) +{ + // ecs.items + lua_newtable(L); + lua_pushcfunction(L, luaItemRegister); + lua_setfield(L, -2, "register"); + lua_pushcfunction(L, luaItemFind); + lua_setfield(L, -2, "find"); + lua_pushcfunction(L, luaItemList); + lua_setfield(L, -2, "list"); + lua_pushcfunction(L, luaItemIsUnique); + lua_setfield(L, -2, "is_unique"); + lua_setfield(L, -2, "items"); + + // ecs.inventory + lua_newtable(L); + lua_pushcfunction(L, luaInventoryAdd); + lua_setfield(L, -2, "add"); + lua_pushcfunction(L, luaInventoryRemove); + lua_setfield(L, -2, "remove"); + lua_pushcfunction(L, luaInventoryHas); + lua_setfield(L, -2, "has"); + lua_pushcfunction(L, luaInventoryCount); + lua_setfield(L, -2, "count"); + lua_pushcfunction(L, luaInventoryGetSlots); + lua_setfield(L, -2, "get_slots"); + lua_pushcfunction(L, luaInventorySetSlots); + lua_setfield(L, -2, "set_slots"); + lua_setfield(L, -2, "inventory"); + + // ecs.container + lua_newtable(L); + lua_pushcfunction(L, luaContainerGetState); + lua_setfield(L, -2, "get_state"); + lua_pushcfunction(L, luaContainerSetState); + lua_setfield(L, -2, "set_state"); + lua_pushcfunction(L, luaContainerClearState); + lua_setfield(L, -2, "clear_state"); + lua_setfield(L, -2, "container"); +} + +} // namespace editScene diff --git a/src/features/editScene/lua/LuaItemApi.hpp b/src/features/editScene/lua/LuaItemApi.hpp new file mode 100644 index 0000000..9508b75 --- /dev/null +++ b/src/features/editScene/lua/LuaItemApi.hpp @@ -0,0 +1,14 @@ +#ifndef EDITSCENE_LUA_ITEM_API_HPP +#define EDITSCENE_LUA_ITEM_API_HPP +#pragma once + +#include + +namespace editScene +{ + +void registerLuaItemApi(lua_State *L); + +} // namespace editScene + +#endif // EDITSCENE_LUA_ITEM_API_HPP diff --git a/src/features/editScene/systems/ActuatorSystem.cpp b/src/features/editScene/systems/ActuatorSystem.cpp index ba22424..0171ed2 100644 --- a/src/features/editScene/systems/ActuatorSystem.cpp +++ b/src/features/editScene/systems/ActuatorSystem.cpp @@ -2,6 +2,7 @@ #include "../EditorApp.hpp" #include "BehaviorTreeSystem.hpp" #include "ItemSystem.hpp" +#include "ItemRegistry.hpp" #include "../components/Actuator.hpp" #include "../components/Item.hpp" #include "../components/Inventory.hpp" @@ -398,7 +399,11 @@ void ActuatorSystem::update(float deltaTime) } else if (targetEntity.has()) { // Show "E - Pick up [ItemName]" for items auto &item = targetEntity.get(); - m_labelText = "E Pick up " + item.itemName; + std::string displayName = ItemRegistry::getSingleton().getItemName( + item.itemId); + if (displayName.empty()) + displayName = item.itemId; + m_labelText = "E Pick up " + displayName; } } } diff --git a/src/features/editScene/systems/BehaviorTreeSystem.cpp b/src/features/editScene/systems/BehaviorTreeSystem.cpp index 0e2aee6..c5d2d61 100644 --- a/src/features/editScene/systems/BehaviorTreeSystem.cpp +++ b/src/features/editScene/systems/BehaviorTreeSystem.cpp @@ -786,70 +786,28 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e, 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"; + // Parse params: "itemId,count" + std::string itemId = node.name.c_str(); 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(), + char *end = nullptr; + long val = strtol(node.params.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); - } + if (end != node.params.c_str() && + *end == '\0' && val > 0) + count = 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; + bool added = itemSystem->addItemToInventory(e, itemId, + count); + if (added) { + std::cout << "[BT] addItemToInventory: added " + << itemId << " x" << count + << std::endl; + return Status::success; + } + return Status::failure; } return Status::success; } diff --git a/src/features/editScene/systems/ContainerStateRegistry.cpp b/src/features/editScene/systems/ContainerStateRegistry.cpp new file mode 100644 index 0000000..6e0df83 --- /dev/null +++ b/src/features/editScene/systems/ContainerStateRegistry.cpp @@ -0,0 +1,125 @@ +#include "ContainerStateRegistry.hpp" +#include +#include + +ContainerStateRegistry &ContainerStateRegistry::getInstance() +{ + static ContainerStateRegistry instance; + return instance; +} + +ContainerStateRegistry::ContainerStateRegistry() +{ + m_autoSavePath = "container_state.json"; +} + +void ContainerStateRegistry::loadState(const std::string &containerId, + const std::vector &slots) +{ + ContainerState &state = m_states[containerId]; + state.containerId = containerId; + state.slots = slots; + autoSave(); +} + +std::vector +ContainerStateRegistry::getState(const std::string &containerId) const +{ + auto it = m_states.find(containerId); + if (it != m_states.end()) + return it->second.slots; + return {}; +} + +bool ContainerStateRegistry::hasState(const std::string &containerId) const +{ + return m_states.find(containerId) != m_states.end(); +} + +void ContainerStateRegistry::clearState(const std::string &containerId) +{ + m_states.erase(containerId); + autoSave(); +} + +nlohmann::json ContainerStateRegistry::serialize() const +{ + nlohmann::json j; + j["version"] = "1.0"; + for (const auto &pair : m_states) { + const ContainerState &state = pair.second; + nlohmann::json stateJson; + stateJson["containerId"] = state.containerId; + nlohmann::json slotsJson = nlohmann::json::array(); + for (const auto &slot : state.slots) { + nlohmann::json slotJson; + slotJson["itemId"] = slot.itemId; + slotJson["stackSize"] = slot.stackSize; + slotsJson.push_back(slotJson); + } + stateJson["slots"] = slotsJson; + j["states"].push_back(stateJson); + } + return j; +} + +void ContainerStateRegistry::deserialize(const nlohmann::json &j) +{ + m_states.clear(); + if (!j.contains("states")) + return; + for (const auto &stateJson : j["states"]) { + ContainerState state; + state.containerId = stateJson.value("containerId", ""); + if (stateJson.contains("slots") && stateJson["slots"].is_array()) { + for (const auto &slotJson : stateJson["slots"]) { + ContainerSlot slot; + slot.itemId = slotJson.value("itemId", ""); + slot.stackSize = slotJson.value("stackSize", 0); + state.slots.push_back(slot); + } + } + if (!state.containerId.empty()) + m_states[state.containerId] = state; + } +} + +bool ContainerStateRegistry::saveToFile(const std::string &filepath) +{ + try { + std::ofstream file(filepath); + if (!file.is_open()) { + m_lastError = "Cannot open " + filepath; + return false; + } + file << serialize().dump(4); + return true; + } catch (const std::exception &e) { + m_lastError = std::string("Save error: ") + e.what(); + return false; + } +} + +bool ContainerStateRegistry::loadFromFile(const std::string &filepath) +{ + try { + std::ifstream file(filepath); + if (!file.is_open()) { + m_lastError = "Cannot open " + filepath; + return false; + } + nlohmann::json j; + file >> j; + deserialize(j); + return true; + } catch (const std::exception &e) { + m_lastError = std::string("Load error: ") + e.what(); + return false; + } +} + +void ContainerStateRegistry::autoSave() +{ + if (!m_autoSavePath.empty()) + saveToFile(m_autoSavePath); +} diff --git a/src/features/editScene/systems/ContainerStateRegistry.hpp b/src/features/editScene/systems/ContainerStateRegistry.hpp new file mode 100644 index 0000000..2eada73 --- /dev/null +++ b/src/features/editScene/systems/ContainerStateRegistry.hpp @@ -0,0 +1,61 @@ +#ifndef EDITSCENE_CONTAINERSTATEREGISTRY_HPP +#define EDITSCENE_CONTAINERSTATEREGISTRY_HPP +#pragma once + +#include +#include +#include +#include + +/** + * Global container state registry. + * + * Stores runtime container contents keyed by containerId. + * Used to override scene initial contents for persistent containers. + * Saves to container_state.json. + */ +class ContainerStateRegistry { +public: + static ContainerStateRegistry &getInstance(); + + struct ContainerSlot { + std::string itemId; + int stackSize = 0; + }; + + struct ContainerState { + std::string containerId; + std::vector slots; + }; + + void loadState(const std::string &containerId, + const std::vector &slots); + std::vector getState(const std::string &containerId) const; + bool hasState(const std::string &containerId) const; + void clearState(const std::string &containerId); + + nlohmann::json serialize() const; + void deserialize(const nlohmann::json &j); + + bool saveToFile(const std::string &filepath); + bool loadFromFile(const std::string &filepath); + const std::string &getLastError() const + { + return m_lastError; + } + + void autoSave(); + +private: + ContainerStateRegistry(); + ~ContainerStateRegistry() = default; + + ContainerStateRegistry(const ContainerStateRegistry &) = delete; + ContainerStateRegistry &operator=(const ContainerStateRegistry &) = delete; + + std::unordered_map m_states; + mutable std::string m_lastError; + std::string m_autoSavePath; +}; + +#endif // EDITSCENE_CONTAINERSTATEREGISTRY_HPP diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 053b918..6905bdd 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -2,6 +2,7 @@ #include "EditorUISystem.hpp" #include "DialogueSystem.hpp" #include "PrefabSystem.hpp" +#include "ItemRegistry.hpp" #include "../camera/EditorCamera.hpp" #include "../components/EntityName.hpp" #include "../components/Transform.hpp" @@ -372,6 +373,11 @@ void EditorUISystem::update(float deltaTime) m_characterRegistry.drawEditor(&m_showCharacterRegistry); } + // Render Item registry window + if (m_showItemRegistry) { + ItemRegistry::getSingleton().drawEditor(&m_showItemRegistry); + } + // Render FPS overlay renderFPSOverlay(deltaTime); } @@ -473,6 +479,9 @@ void EditorUISystem::renderHierarchyWindow() "Character Registry")) { m_showCharacterRegistry = true; } + if (ImGui::MenuItem("Item Registry")) { + m_showItemRegistry = true; + } ImGui::EndMenu(); } diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index 6f9a719..c25fc98 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -316,6 +316,9 @@ private: bool m_showCharacterRegistry = false; CharacterRegistry m_characterRegistry; + // Item registry + bool m_showItemRegistry = false; + // Queries flecs::query m_nameQuery; diff --git a/src/features/editScene/systems/ItemRegistry.cpp b/src/features/editScene/systems/ItemRegistry.cpp new file mode 100644 index 0000000..882cdce --- /dev/null +++ b/src/features/editScene/systems/ItemRegistry.cpp @@ -0,0 +1,371 @@ +#include "ItemRegistry.hpp" +#include +#include +#include +#include + +/* ===================================================================== */ +/* Singleton */ +/* ===================================================================== */ + +ItemRegistry *ItemRegistry::ms_singleton = nullptr; + +ItemRegistry &ItemRegistry::getSingleton() +{ + OgreAssert(ms_singleton, "ItemRegistry not created"); + return *ms_singleton; +} + +ItemRegistry *ItemRegistry::getSingletonPtr() +{ + return ms_singleton; +} + +/* ===================================================================== */ +/* Construction / init */ +/* ===================================================================== */ + +ItemRegistry::ItemRegistry() +{ + ms_singleton = this; + m_autoSavePath = "items.json"; +} + +void ItemRegistry::initialize() +{ + if (!std::filesystem::exists(m_autoSavePath)) + return; + if (!loadFromFile(m_autoSavePath)) { + Ogre::LogManager::getSingleton().logMessage( + "ItemRegistry: auto-load failed: " + m_lastError); + } +} + +void ItemRegistry::autoSave() +{ + if (!m_autoSavePath.empty()) + saveToFile(m_autoSavePath); +} + +/* ===================================================================== */ +/* Definitions */ +/* ===================================================================== */ + +bool ItemRegistry::registerItem(const ItemRegistry::ItemDefinition &def) +{ + if (def.itemId.empty()) { + m_lastError = "Cannot register item with empty itemId"; + return false; + } + m_definitions[def.itemId] = def; + autoSave(); + return true; +} + +bool ItemRegistry::removeItem(const std::string &itemId) +{ + auto it = m_definitions.find(itemId); + if (it == m_definitions.end()) { + m_lastError = "Item not found: " + itemId; + return false; + } + m_definitions.erase(it); + autoSave(); + return true; +} + +ItemRegistry::ItemDefinition *ItemRegistry::findDefinition(const std::string &itemId) +{ + auto it = m_definitions.find(itemId); + if (it != m_definitions.end()) + return &it->second; + return nullptr; +} + +const ItemRegistry::ItemDefinition *ItemRegistry::findDefinition(const std::string &itemId) const +{ + auto it = m_definitions.find(itemId); + if (it != m_definitions.end()) + return &it->second; + return nullptr; +} + +bool ItemRegistry::isUnique(const std::string &itemId) const +{ + auto *def = findDefinition(itemId); + return def && def->unique; +} + +std::string ItemRegistry::getItemName(const std::string &itemId) const +{ + auto *def = findDefinition(itemId); + return def ? def->itemName : ""; +} + +std::string ItemRegistry::getItemType(const std::string &itemId) const +{ + auto *def = findDefinition(itemId); + return def ? def->itemType : ""; +} + +int ItemRegistry::getMaxStackSize(const std::string &itemId) const +{ + auto *def = findDefinition(itemId); + return def ? def->maxStackSize : 99; +} + +float ItemRegistry::getWeight(const std::string &itemId) const +{ + auto *def = findDefinition(itemId); + return def ? def->weight : 0.1f; +} + +int ItemRegistry::getValue(const std::string &itemId) const +{ + auto *def = findDefinition(itemId); + return def ? def->value : 1; +} + +std::string ItemRegistry::getUseActionName(const std::string &itemId) const +{ + auto *def = findDefinition(itemId); + return def ? def->useActionName : ""; +} + +/* ===================================================================== */ +/* Columns */ +/* ===================================================================== */ + +void ItemRegistry::addColumn(const std::string &name, ColumnDef::Type type) +{ + m_columns.push_back({type, name}); +} + +void ItemRegistry::removeColumn(const std::string &name) +{ + m_columns.erase( + std::remove_if(m_columns.begin(), m_columns.end(), + [&name](const ColumnDef &c) { + return c.name == name; + }), + m_columns.end()); +} + +/* ===================================================================== */ +/* Persistence */ +/* ===================================================================== */ + +nlohmann::json ItemRegistry::serialize() const +{ + nlohmann::json j; + j["version"] = "1.0"; + + for (const auto &c : m_columns) { + nlohmann::json col; + col["name"] = c.name; + col["type"] = (c.type == ColumnDef::Int) ? "int" : + (c.type == ColumnDef::Float) ? "float" : + "string"; + j["columns"].push_back(col); + } + + for (const auto &pair : m_definitions) { + const ItemRegistry::ItemDefinition &d = pair.second; + nlohmann::json def; + def["itemId"] = d.itemId; + def["itemName"] = d.itemName; + def["itemType"] = d.itemType; + def["maxStackSize"] = d.maxStackSize; + def["weight"] = d.weight; + def["value"] = d.value; + def["useActionName"] = d.useActionName; + def["unique"] = d.unique; + for (const auto &kv : d.intColumns) + def["intColumns"][kv.first] = kv.second; + for (const auto &kv : d.floatColumns) + def["floatColumns"][kv.first] = kv.second; + for (const auto &kv : d.stringColumns) + def["stringColumns"][kv.first] = kv.second; + j["definitions"].push_back(def); + } + + return j; +} + +void ItemRegistry::deserialize(const nlohmann::json &j) +{ + m_definitions.clear(); + m_columns.clear(); + + if (j.contains("columns")) { + for (const auto &col : j["columns"]) { + ColumnDef::Type t = ColumnDef::String; + std::string ts = col.value("type", "string"); + if (ts == "int") + t = ColumnDef::Int; + else if (ts == "float") + t = ColumnDef::Float; + m_columns.push_back({t, col.value("name", "")}); + } + } + + if (j.contains("definitions")) { + for (const auto &defJson : j["definitions"]) { + ItemRegistry::ItemDefinition d; + d.itemId = defJson.value("itemId", ""); + d.itemName = defJson.value("itemName", ""); + d.itemType = defJson.value("itemType", "misc"); + d.maxStackSize = defJson.value("maxStackSize", 99); + d.weight = defJson.value("weight", 0.1f); + d.value = defJson.value("value", 1); + d.useActionName = defJson.value("useActionName", ""); + d.unique = defJson.value("unique", false); + if (defJson.contains("intColumns")) { + for (auto &[key, val] : defJson["intColumns"].items()) + d.intColumns[key] = val.get(); + } + if (defJson.contains("floatColumns")) { + for (auto &[key, val] : defJson["floatColumns"].items()) + d.floatColumns[key] = val.get(); + } + if (defJson.contains("stringColumns")) { + for (auto &[key, val] : defJson["stringColumns"].items()) + d.stringColumns[key] = val.get(); + } + if (!d.itemId.empty()) + m_definitions[d.itemId] = d; + } + } +} + +bool ItemRegistry::saveToFile(const std::string &filepath) +{ + try { + std::ofstream file(filepath); + if (!file.is_open()) { + m_lastError = "Cannot open " + filepath; + return false; + } + file << serialize().dump(4); + return true; + } catch (const std::exception &e) { + m_lastError = std::string("Save error: ") + e.what(); + return false; + } +} + +bool ItemRegistry::loadFromFile(const std::string &filepath) +{ + try { + std::ifstream file(filepath); + if (!file.is_open()) { + m_lastError = "Cannot open " + filepath; + return false; + } + nlohmann::json j; + file >> j; + deserialize(j); + return true; + } catch (const std::exception &e) { + m_lastError = std::string("Load error: ") + e.what(); + return false; + } +} + +/* ===================================================================== */ +/* ImGui editor */ +/* ===================================================================== */ + +void ItemRegistry::drawEditor(bool *p_open) +{ + if (p_open && !*p_open) + return; + + ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Item Registry", p_open)) + return ImGui::End(); + + // Left pane: list + ImGui::BeginChild("ItemList", ImVec2(250, 0), true); + + if (ImGui::Button("Add Item")) { + std::string newId = m_newItemIdBuf; + if (!newId.empty() && m_definitions.find(newId) == m_definitions.end()) { + ItemRegistry::ItemDefinition d; + d.itemId = newId; + d.itemName = m_newItemNameBuf[0] ? m_newItemNameBuf : newId; + registerItem(d); + m_newItemIdBuf[0] = '\0'; + m_newItemNameBuf[0] = '\0'; + } + } + ImGui::InputText("New ID", m_newItemIdBuf, sizeof(m_newItemIdBuf)); + ImGui::InputText("New Name", m_newItemNameBuf, sizeof(m_newItemNameBuf)); + ImGui::Separator(); + + int idx = 0; + for (const auto &pair : m_definitions) { + const ItemRegistry::ItemDefinition &d = pair.second; + bool selected = (idx == m_selectedItemIndex); + if (ImGui::Selectable( + (d.itemId + " - " + d.itemName).c_str(), selected)) { + m_selectedItemIndex = idx; + } + idx++; + } + ImGui::EndChild(); + + ImGui::SameLine(); + + // Right pane: edit + ImGui::BeginChild("ItemEdit", ImVec2(0, 0), true); + + ItemRegistry::ItemDefinition *selectedDef = nullptr; + idx = 0; + for (auto &pair : m_definitions) { + if (idx == m_selectedItemIndex) { + selectedDef = &pair.second; + break; + } + idx++; + } + + if (selectedDef) { + ImGui::Text("Item ID: %s", selectedDef->itemId.c_str()); + static char nameBuf[256]; + static char typeBuf[256]; + static char actionBuf[256]; + snprintf(nameBuf, sizeof(nameBuf), "%s", + selectedDef->itemName.c_str()); + snprintf(typeBuf, sizeof(typeBuf), "%s", + selectedDef->itemType.c_str()); + snprintf(actionBuf, sizeof(actionBuf), "%s", + selectedDef->useActionName.c_str()); + if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) + selectedDef->itemName = nameBuf; + if (ImGui::InputText("Type", typeBuf, sizeof(typeBuf))) + selectedDef->itemType = typeBuf; + if (ImGui::InputInt("Max Stack", &selectedDef->maxStackSize)) + selectedDef->maxStackSize = std::max(1, selectedDef->maxStackSize); + ImGui::InputFloat("Weight", &selectedDef->weight, 0.1f); + ImGui::InputInt("Value", &selectedDef->value); + if (ImGui::InputText("Use Action", actionBuf, sizeof(actionBuf))) + selectedDef->useActionName = actionBuf; + if (ImGui::Checkbox("Unique", &selectedDef->unique)) + autoSave(); + + if (ImGui::Button("Save Changes")) + autoSave(); + ImGui::SameLine(); + if (ImGui::Button("Delete Item")) { + removeItem(selectedDef->itemId); + m_selectedItemIndex = -1; + } + } else { + ImGui::Text("Select an item to edit"); + } + + ImGui::EndChild(); + + ImGui::End(); +} diff --git a/src/features/editScene/systems/ItemRegistry.hpp b/src/features/editScene/systems/ItemRegistry.hpp new file mode 100644 index 0000000..73e3986 --- /dev/null +++ b/src/features/editScene/systems/ItemRegistry.hpp @@ -0,0 +1,137 @@ +#ifndef EDITSCENE_ITEMREGISTRY_HPP +#define EDITSCENE_ITEMREGISTRY_HPP +#pragma once + +#include +#include +#include +#include +#include + +class EditorApp; + +/** + * Global item definition registry. + * + * Stores authoritative item definitions keyed by itemId. + * Auto-saves to items.json after mutations. + * Loaded on initialization. + */ +class ItemRegistry { +public: + static ItemRegistry &getSingleton(); + static ItemRegistry *getSingletonPtr(); + +private: + static ItemRegistry *ms_singleton; + +public: + /* ------------------------------------------------------------------ */ + /* Column schema */ + /* ------------------------------------------------------------------ */ + struct ColumnDef { + enum Type { Int, Float, String }; + Type type; + std::string name; + }; + + /* ------------------------------------------------------------------ */ + /* Item definition */ + /* ------------------------------------------------------------------ */ + struct ItemDefinition { + std::string itemId; + std::string itemName; + std::string itemType; + int maxStackSize = 99; + float weight = 0.1f; + int value = 1; + std::string useActionName; + bool unique = false; + + /* Extensible custom columns */ + std::unordered_map intColumns; + std::unordered_map floatColumns; + std::unordered_map stringColumns; + }; + + /* ------------------------------------------------------------------ */ + /* Life-cycle */ + /* ------------------------------------------------------------------ */ + ItemRegistry(); + ~ItemRegistry() = default; + + ItemRegistry(const ItemRegistry &) = delete; + ItemRegistry &operator=(const ItemRegistry &) = delete; + + void initialize(); + + /* ------------------------------------------------------------------ */ + /* Definitions */ + /* ------------------------------------------------------------------ */ + bool registerItem(const ItemDefinition &def); + bool removeItem(const std::string &itemId); + ItemDefinition *findDefinition(const std::string &itemId); + const ItemDefinition *findDefinition(const std::string &itemId) const; + const std::unordered_map &getDefinitions() const + { + return m_definitions; + } + + bool hasItem(const std::string &itemId) const + { + return m_definitions.find(itemId) != m_definitions.end(); + } + + bool isUnique(const std::string &itemId) const; + + /* Lookup helpers */ + std::string getItemName(const std::string &itemId) const; + std::string getItemType(const std::string &itemId) const; + int getMaxStackSize(const std::string &itemId) const; + float getWeight(const std::string &itemId) const; + int getValue(const std::string &itemId) const; + std::string getUseActionName(const std::string &itemId) const; + + /* ------------------------------------------------------------------ */ + /* Columns */ + /* ------------------------------------------------------------------ */ + void addColumn(const std::string &name, ColumnDef::Type type); + void removeColumn(const std::string &name); + const std::vector &getColumns() const + { + return m_columns; + } + + /* ------------------------------------------------------------------ */ + /* Persistence */ + /* ------------------------------------------------------------------ */ + nlohmann::json serialize() const; + void deserialize(const nlohmann::json &j); + + bool saveToFile(const std::string &filepath); + bool loadFromFile(const std::string &filepath); + const std::string &getLastError() const + { + return m_lastError; + } + + void autoSave(); + + /* ------------------------------------------------------------------ */ + /* ImGui editor */ + /* ------------------------------------------------------------------ */ + void drawEditor(bool *p_open = nullptr); + +private: + std::unordered_map m_definitions; + std::vector m_columns; + mutable std::string m_lastError; + std::string m_autoSavePath; + + /* UI state */ + int m_selectedItemIndex = -1; + char m_newItemIdBuf[128] = {}; + char m_newItemNameBuf[128] = {}; +}; + +#endif // EDITSCENE_ITEMREGISTRY_HPP diff --git a/src/features/editScene/systems/ItemSystem.cpp b/src/features/editScene/systems/ItemSystem.cpp index 372e9f4..43d631c 100644 --- a/src/features/editScene/systems/ItemSystem.cpp +++ b/src/features/editScene/systems/ItemSystem.cpp @@ -1,4 +1,6 @@ #include "ItemSystem.hpp" +#include "ItemRegistry.hpp" +#include "ContainerStateRegistry.hpp" #include "../EditorApp.hpp" #include "BehaviorTreeSystem.hpp" #include "../components/Item.hpp" @@ -24,32 +26,29 @@ 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) + const std::string &itemId, int stackSize) { if (!inventoryEntity.is_alive() || !inventoryEntity.has()) return false; + if (itemId.empty() || stackSize <= 0) + return false; auto &inv = inventoryEntity.get_mut(); + int maxStack = ItemRegistry::getSingleton().getMaxStackSize(itemId); // 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; - } + for (int i = 0; i < (int)inv.slots.size(); i++) { + auto &slot = inv.slots[i]; + if (!slot.isEmpty() && slot.itemId == itemId && + slot.stackSize < maxStack) { + int space = maxStack - slot.stackSize; + int add = std::min(space, stackSize); + slot.stackSize += add; + stackSize -= add; + if (stackSize <= 0) { + inv.recalculateWeight(); + return true; } } } @@ -67,14 +66,8 @@ bool ItemSystem::addItemToInventory(flecs::entity inventoryEntity, 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); + int add = std::min(stackSize, maxStack); slot.stackSize = add; stackSize -= add; } @@ -94,13 +87,19 @@ bool ItemSystem::addItemEntityToInventory(flecs::entity inventoryEntity, 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); + item.stackSize); if (result) { + // Store the world entity reference in the first matching slot + auto &inv = inventoryEntity.get_mut(); + for (auto &slot : inv.slots) { + if (slot.itemId == item.itemId && slot.itemEntity == 0) { + slot.itemEntity = itemEntity.id(); + break; + } + } + // Hide the world entity if (itemEntity.has()) { auto &trans = itemEntity.get_mut(); @@ -113,7 +112,7 @@ bool ItemSystem::addItemEntityToInventory(flecs::entity inventoryEntity, } int ItemSystem::removeItemFromInventory(flecs::entity inventoryEntity, - const Ogre::String &itemId, int count) + const std::string &itemId, int count) { if (!inventoryEntity.is_alive() || !inventoryEntity.has()) @@ -187,10 +186,8 @@ bool ItemSystem::transferItem(flecs::entity fromInventory, int slotIndex, 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); + bool added = addItemToInventory(toInventory, slot.itemId, + transferCount); if (!added) return false; @@ -205,7 +202,7 @@ bool ItemSystem::transferItem(flecs::entity fromInventory, int slotIndex, } bool ItemSystem::hasItem(flecs::entity inventoryEntity, - const Ogre::String &itemId) const + const std::string &itemId) const { if (!inventoryEntity.is_alive() || !inventoryEntity.has()) @@ -215,18 +212,28 @@ bool ItemSystem::hasItem(flecs::entity inventoryEntity, } bool ItemSystem::hasItemByName(flecs::entity inventoryEntity, - const Ogre::String &itemName) const + const std::string &itemName) const { if (!inventoryEntity.is_alive() || !inventoryEntity.has()) return false; - return inventoryEntity.get().hasItemByName( - itemName); + // Look up itemId by display name in registry + std::string foundId; + for (const auto &pair : ItemRegistry::getSingleton().getDefinitions()) { + if (pair.second.itemName == itemName) { + foundId = pair.second.itemId; + break; + } + } + if (foundId.empty()) + return false; + + return inventoryEntity.get().hasItem(foundId); } int ItemSystem::countItem(flecs::entity inventoryEntity, - const Ogre::String &itemId) const + const std::string &itemId) const { if (!inventoryEntity.is_alive() || !inventoryEntity.has()) @@ -251,6 +258,8 @@ bool ItemSystem::dropItem(flecs::entity inventoryEntity, int slotIndex, return false; int dropCount = std::min(count, slot.stackSize); + const std::string itemName = + ItemRegistry::getSingleton().getItemName(slot.itemId); // If the item has a world entity reference, show it if (slot.itemEntity != 0) { @@ -273,14 +282,7 @@ bool ItemSystem::dropItem(flecs::entity inventoryEntity, int slotIndex, } 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; + itemEntity.set(ItemComponent(slot.itemId, dropCount)); // Create a scene node for the dropped item Ogre::SceneNode *node = @@ -292,7 +294,7 @@ bool ItemSystem::dropItem(flecs::entity inventoryEntity, int slotIndex, itemEntity.set(trans); EntityNameComponent nameComp; - nameComp.name = slot.itemName + "_dropped"; + nameComp.name = itemName + "_dropped"; itemEntity.set(nameComp); } @@ -317,17 +319,19 @@ bool ItemSystem::useItem(flecs::entity characterEntity, return false; const auto &slot = inv.slots[slotIndex]; - if (slot.isEmpty() || slot.useActionName.empty()) + if (slot.isEmpty()) + return false; + + std::string useAction = + ItemRegistry::getSingleton().getUseActionName(slot.itemId); + if (useAction.empty()) return false; // Execute the use action via behavior tree if (m_btSystem && characterEntity.is_alive()) { - // Look up the action in the singleton database ActionDatabase *db = ActionDatabase::getSingletonPtr(); - if (db) { - const GoapAction *action = - db->findAction(slot.useActionName); + const GoapAction *action = db->findAction(useAction); if (action) { m_btSystem->evaluatePlayerAction( characterEntity.id(), @@ -353,7 +357,8 @@ bool ItemSystem::pickupItem(flecs::entity characterEntity, if (result) { Ogre::LogManager::getSingleton().logMessage( "[ItemSystem] Picked up: " + - itemEntity.get().itemName); + ItemRegistry::getSingleton().getItemName( + itemEntity.get().itemId)); } return result; } diff --git a/src/features/editScene/systems/ItemSystem.hpp b/src/features/editScene/systems/ItemSystem.hpp index 6a63aeb..19d4aa5 100644 --- a/src/features/editScene/systems/ItemSystem.hpp +++ b/src/features/editScene/systems/ItemSystem.hpp @@ -12,11 +12,8 @@ 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. + * Provides a pure API for inventory operations. All item properties + * are looked up from ItemRegistry by itemId. */ class ItemSystem { public: @@ -28,11 +25,7 @@ public: /** 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 = ""); + const std::string &itemId, int stackSize = 1); /** Add an item entity (ItemComponent) to an inventory. */ bool addItemEntityToInventory(flecs::entity inventoryEntity, @@ -40,7 +33,7 @@ public: /** Remove items from inventory by itemId. Returns number removed. */ int removeItemFromInventory(flecs::entity inventoryEntity, - const Ogre::String &itemId, int count = 1); + const std::string &itemId, int count = 1); /** Remove items from inventory by slot index. */ bool removeItemFromSlot(flecs::entity inventoryEntity, int slotIndex, @@ -52,15 +45,15 @@ public: /** Check if an inventory has at least one of a specific itemId. */ bool hasItem(flecs::entity inventoryEntity, - const Ogre::String &itemId) const; + const std::string &itemId) const; - /** Check if an inventory has at least one of a specific itemName. */ + /** Check if an inventory has an item with the given display name. */ bool hasItemByName(flecs::entity inventoryEntity, - const Ogre::String &itemName) const; + const std::string &itemName) const; /** Count how many of a specific itemId are in an inventory. */ int countItem(flecs::entity inventoryEntity, - const Ogre::String &itemId) const; + const std::string &itemId) const; /** Drop an item from inventory into the world at a position. */ bool dropItem(flecs::entity inventoryEntity, int slotIndex, diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index b380047..1f95064 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -1,4 +1,6 @@ #include "SceneSerializer.hpp" +#include "ItemRegistry.hpp" +#include "ContainerStateRegistry.hpp" #include "../components/Transform.hpp" #include "../components/Renderable.hpp" #include "../components/EntityName.hpp" @@ -3827,14 +3829,8 @@ 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; } @@ -3846,19 +3842,14 @@ nlohmann::json SceneSerializer::serializeInventory(flecs::entity entity) json["maxWeight"] = inv.maxWeight; json["isContainer"] = inv.isContainer; json["isOpen"] = inv.isOpen; + json["containerId"] = inv.containerId; 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; @@ -3866,18 +3857,38 @@ nlohmann::json SceneSerializer::serializeInventory(flecs::entity entity) return json; } +static void ensureItemInRegistry(const std::string &itemId, + const nlohmann::json &json) +{ + if (itemId.empty()) + return; + ItemRegistry *reg = ItemRegistry::getSingletonPtr(); + if (!reg || reg->hasItem(itemId)) + return; + ItemRegistry::ItemDefinition def; + def.itemId = itemId; + def.itemName = json.value("itemName", itemId); + def.itemType = json.value("itemType", "misc"); + def.maxStackSize = json.value("maxStackSize", 99); + def.weight = json.value("weight", 0.1f); + def.value = json.value("value", 1); + def.useActionName = json.value("useActionName", ""); + def.unique = json.value("unique", false); + reg->registerItem(def); +} + 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", ""); + + // Backward compatibility: old scenes had inline item data + if (item.itemId.empty() && json.contains("itemName")) { + item.itemId = json.value("itemName", ""); + } + ensureItemInRegistry(item.itemId, json); entity.set(item); } @@ -3889,6 +3900,7 @@ void SceneSerializer::deserializeInventory(flecs::entity entity, inv.maxWeight = json.value("maxWeight", 50.0f); inv.isContainer = json.value("isContainer", false); inv.isOpen = json.value("isOpen", false); + inv.containerId = json.value("containerId", ""); inv.slots.clear(); if (json.contains("slots") && json["slots"].is_array()) { @@ -3896,19 +3908,35 @@ void SceneSerializer::deserializeInventory(flecs::entity entity, 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", ""); + + // Backward compatibility: old slots had inline data + if (slot.itemId.empty() && slotJson.contains("itemName")) { + slot.itemId = slotJson.value("itemName", ""); + } + ensureItemInRegistry(slot.itemId, slotJson); inv.slots.push_back(slot); } } inv.recalculateWeight(); entity.set(inv); + + // For containers with a containerId, check global state override + if (inv.isContainer && !inv.containerId.empty()) { + auto &cstate = ContainerStateRegistry::getInstance(); + if (cstate.hasState(inv.containerId)) { + auto stateSlots = cstate.getState(inv.containerId); + inv.slots.clear(); + for (const auto &s : stateSlots) { + InventorySlot slot; + slot.itemId = s.itemId; + slot.stackSize = s.stackSize; + inv.slots.push_back(slot); + } + inv.recalculateWeight(); + entity.set(inv); + } + } } diff --git a/src/features/editScene/tests/component_lua_test.cpp b/src/features/editScene/tests/component_lua_test.cpp index 6a4388b..2ca6961 100644 --- a/src/features/editScene/tests/component_lua_test.cpp +++ b/src/features/editScene/tests/component_lua_test.cpp @@ -562,31 +562,19 @@ static int testNavMeshGeometrySourceTag(lua_State *L) static int testItemComponent(lua_State *L) { - TEST("Item component with all fields"); + TEST("Item component"); bool ok = runLua( L, "local id = ecs.create_entity();" "ecs.set_component(id, 'Item', {" - " itemName = 'Health Potion'," - " itemType = 'consumable'," " itemId = 'potion_health'," - " stackSize = 1," - " maxStackSize = 10," - " weight = 0.5," - " value = 50," - " useActionName = 'drink_potion'" + " stackSize = 5" "});" "local item = ecs.get_component(id, 'Item');" "assert(item ~= nil, 'Item should exist');" - "assert(item.itemName == 'Health Potion', 'wrong name');" - "assert(item.itemType == 'consumable', 'wrong type');" "assert(item.itemId == 'potion_health', 'wrong id');" - "assert(item.stackSize == 1, 'wrong stackSize');" - "assert(item.maxStackSize == 10, 'wrong maxStackSize');" - "assert(item.weight == 0.5, 'wrong weight');" - "assert(item.value == 50, 'wrong value');" - "assert(item.useActionName == 'drink_potion', 'wrong useActionName')"); + "assert(item.stackSize == 5, 'wrong stackSize')"); if (!ok) FAIL("Item component assertion failed"); diff --git a/src/features/editScene/tests/item_lua_test.cpp b/src/features/editScene/tests/item_lua_test.cpp new file mode 100644 index 0000000..8048cdf --- /dev/null +++ b/src/features/editScene/tests/item_lua_test.cpp @@ -0,0 +1,373 @@ +/** + * @file item_lua_test.cpp + * @brief Standalone test for the Lua Item / Inventory / Container APIs. + * + * Tests the ecs.items.*, ecs.inventory.*, and ecs.container.* functions + * exposed via the ecs.* Lua API. + * + * Build with: + * g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \ + * item_lua_test.cpp \ + * lua_test_stubs.cpp \ + * ../../lua/lua-5.4.8/src/liblua.a \ + * -o item_lua_test -lm + * + * Or via CMake (see CMakeLists.txt in this directory). + */ + +#include +#include +#include +#include +#include + +// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager) +#include "ogre_stub.h" + +// Include Lua +extern "C" { +#include +#include +#include +} + +// Forward declare the registration function +namespace editScene +{ +void registerLuaItemApi(lua_State *L); +} + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +static int testCount = 0; +static int passCount = 0; + +#define TEST(name) \ + do { \ + testCount++; \ + printf(" TEST %d: %s ... ", testCount, name); \ + } while (0) + +#define PASS() \ + do { \ + passCount++; \ + printf("PASS\n"); \ + } while (0) + +#define FAIL(msg) \ + do { \ + printf("FAIL: %s\n", msg); \ + return 1; \ + } while (0) + +// --------------------------------------------------------------------------- +// Helper: run a Lua string and check for errors +// --------------------------------------------------------------------------- + +static bool runLua(lua_State *L, const char *code) +{ + if (luaL_dostring(L, code) != LUA_OK) { + fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// Test 1: Register and find an item +// --------------------------------------------------------------------------- + +static int testItemRegisterAndFind(lua_State *L) +{ + TEST("register and find item definition"); + + bool ok = runLua(L, + "ecs.items.register('potion_health', {" + " itemName = 'Health Potion'," + " itemType = 'consumable'," + " maxStackSize = 10," + " weight = 0.5," + " value = 25," + " useActionName = 'drink_potion'," + " unique = false" + "}); " + "local def = ecs.items.find('potion_health');" + "assert(def ~= nil, 'definition should exist');" + "assert(def.itemName == 'Health Potion', 'wrong name');" + "assert(def.itemType == 'consumable', 'wrong type');" + "assert(def.maxStackSize == 10, 'wrong maxStackSize');" + "assert(def.weight == 0.5, 'wrong weight');" + "assert(def.value == 25, 'wrong value');" + "assert(def.useActionName == 'drink_potion', 'wrong action');" + "assert(def.unique == false, 'wrong unique')"); + if (!ok) + FAIL("register/find assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 2: List registered items +// --------------------------------------------------------------------------- + +static int testItemList(lua_State *L) +{ + TEST("list registered items"); + + bool ok = runLua(L, + "ecs.items.register('sword_iron', { itemName = 'Iron Sword' });" + "local list = ecs.items.list();" + "assert(#list >= 2, 'expected at least 2 items');" + "local found = false;" + "for _, id in ipairs(list) do " + " if id == 'sword_iron' then found = true end " + "end; " + "assert(found, 'sword_iron should be in list')"); + if (!ok) + FAIL("list assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 3: is_unique returns correct value +// --------------------------------------------------------------------------- + +static int testItemIsUnique(lua_State *L) +{ + TEST("is_unique returns correct value"); + + bool ok = runLua(L, + "ecs.items.register('amulet_legendary', {" + " itemName = 'Legendary Amulet'," + " unique = true" + "}); " + "assert(ecs.items.is_unique('amulet_legendary') == true, " + " 'amulet should be unique'); " + "assert(ecs.items.is_unique('potion_health') == false, " + " 'potion should not be unique'); " + "assert(ecs.items.is_unique('nonexistent') == false, " + " 'nonexistent should return false')"); + if (!ok) + FAIL("is_unique assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 4: Find nonexistent item returns nil +// --------------------------------------------------------------------------- + +static int testItemFindMissing(lua_State *L) +{ + TEST("find nonexistent item returns nil"); + + bool ok = runLua(L, + "local def = ecs.items.find('does_not_exist');" + "assert(def == nil, 'should be nil for missing item')"); + if (!ok) + FAIL("find missing assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 5: Inventory add and count +// --------------------------------------------------------------------------- + +static int testInventoryAddAndCount(lua_State *L) +{ + TEST("inventory add and count"); + + bool ok = runLua(L, + "local id = 42; " + "local ok = ecs.inventory.add(id, 'potion_health', 3); " + "assert(ok == true, 'add should return true'); " + "local count = ecs.inventory.count(id, 'potion_health'); " + "assert(count == 3, 'expected count 3, got ' .. tostring(count))"); + if (!ok) + FAIL("add/count assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 6: Inventory has item +// --------------------------------------------------------------------------- + +static int testInventoryHas(lua_State *L) +{ + TEST("inventory has item"); + + bool ok = runLua(L, + "local id = 42; " + "ecs.inventory.add(id, 'sword_iron', 1); " + "assert(ecs.inventory.has(id, 'sword_iron') == true, " + " 'should have sword_iron'); " + "assert(ecs.inventory.has(id, 'nonexistent') == false, " + " 'should not have nonexistent')"); + if (!ok) + FAIL("has assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 7: Inventory remove +// --------------------------------------------------------------------------- + +static int testInventoryRemove(lua_State *L) +{ + TEST("inventory remove"); + + bool ok = runLua(L, + "local id = 42; " + "ecs.inventory.add(id, 'potion_health', 10); " + "local removed = ecs.inventory.remove(id, 'potion_health', 4); " + "assert(removed == 4, 'expected removed 4, got ' .. tostring(removed)); " + "local count = ecs.inventory.count(id, 'potion_health'); " + "assert(count == 9, 'expected 9 remaining (3+10-4), got ' .. tostring(count))"); + if (!ok) + FAIL("remove assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 8: Inventory get_slots +// --------------------------------------------------------------------------- + +static int testInventoryGetSlots(lua_State *L) +{ + TEST("inventory get_slots"); + + bool ok = runLua(L, + "local id = 100; " + "ecs.inventory.add(id, 'gem_ruby', 5); " + "ecs.inventory.add(id, 'gem_sapphire', 3); " + "local slots = ecs.inventory.get_slots(id); " + "assert(#slots >= 2, 'expected at least 2 slots'); " + "local rubyFound = false; " + "for _, slot in ipairs(slots) do " + " if slot.itemId == 'gem_ruby' then " + " assert(slot.stackSize == 5, 'wrong ruby count'); " + " rubyFound = true; " + " end " + "end; " + "assert(rubyFound, 'ruby slot not found')"); + if (!ok) + FAIL("get_slots assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 9: Inventory set_slots +// --------------------------------------------------------------------------- + +static int testInventorySetSlots(lua_State *L) +{ + TEST("inventory set_slots"); + + bool ok = runLua(L, + "local id = 200; " + "ecs.inventory.set_slots(id, {" + " { itemId = 'arrow', stackSize = 99 }," + " { itemId = 'bow', stackSize = 1 }" + "}); " + "local slots = ecs.inventory.get_slots(id); " + "assert(#slots == 2, 'expected 2 slots'); " + "assert(ecs.inventory.count(id, 'arrow') == 99, 'wrong arrow count'); " + "assert(ecs.inventory.count(id, 'bow') == 1, 'wrong bow count')"); + if (!ok) + FAIL("set_slots assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 10: Container get/set/clear state +// --------------------------------------------------------------------------- + +static int testContainerState(lua_State *L) +{ + TEST("container get/set/clear state"); + + bool ok = runLua(L, + "ecs.container.set_state('chest_village', {" + " { itemId = 'gold_coin', stackSize = 50 }," + " { itemId = 'iron_dagger', stackSize = 1 }" + "}); " + "local state = ecs.container.get_state('chest_village'); " + "assert(#state == 2, 'expected 2 slots'); " + "local foundCoin = false; " + "for _, s in ipairs(state) do " + " if s.itemId == 'gold_coin' then " + " assert(s.stackSize == 50, 'wrong coin count'); " + " foundCoin = true; " + " end " + "end; " + "assert(foundCoin, 'gold_coin not found'); " + "ecs.container.clear_state('chest_village'); " + "local cleared = ecs.container.get_state('chest_village'); " + "assert(#cleared == 0, 'expected empty after clear')"); + if (!ok) + FAIL("container state assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +int main() +{ + printf("Item / Inventory / Container Lua API Tests\n"); + printf("==========================================\n\n"); + + // Create Lua state + lua_State *L = luaL_newstate(); + if (!L) { + fprintf(stderr, "Failed to create Lua state\n"); + return 1; + } + luaL_openlibs(L); + + // Register the item API + editScene::registerLuaItemApi(L); + + // Run tests + int failures = 0; + failures += testItemRegisterAndFind(L); + failures += testItemList(L); + failures += testItemIsUnique(L); + failures += testItemFindMissing(L); + failures += testInventoryAddAndCount(L); + failures += testInventoryHas(L); + failures += testInventoryRemove(L); + failures += testInventoryGetSlots(L); + failures += testInventorySetSlots(L); + failures += testContainerState(L); + + // Cleanup + lua_close(L); + + printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount, + failures); + + return failures > 0 ? 1 : 0; +} diff --git a/src/features/editScene/tests/lua_test_stubs.cpp b/src/features/editScene/tests/lua_test_stubs.cpp index f2f92ba..a811b4a 100644 --- a/src/features/editScene/tests/lua_test_stubs.cpp +++ b/src/features/editScene/tests/lua_test_stubs.cpp @@ -20,6 +20,7 @@ #include #include #include +#include // --------------------------------------------------------------------------- // Shared component storage (used by both entity and component API stubs) @@ -222,6 +223,351 @@ void registerLuaDialogueApi(lua_State *L) lua_setglobal(L, "ecs"); } +// --------------------------------------------------------------------------- +// Stub: LuaItemApi +// --------------------------------------------------------------------------- + +struct StubItemDef { + std::string itemId; + std::string itemName; + std::string itemType; + int maxStackSize = 99; + float weight = 0.1f; + int value = 1; + std::string useActionName; + bool unique = false; +}; + +static std::unordered_map s_stubItems; + +struct StubInvSlot { + std::string itemId; + int stackSize = 0; +}; + +static std::unordered_map > s_stubInventories; +static std::unordered_map > s_stubContainerStates; + +void registerLuaItemApi(lua_State *L) +{ + lua_getglobal(L, "ecs"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + + // ecs.items + lua_newtable(L); + + // items.register + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 2 || !lua_isstring(L, 1) || !lua_istable(L, 2)) + return 0; + std::string itemId = lua_tostring(L, 1); + StubItemDef def; + def.itemId = itemId; + if (lua_getfield(L, 2, "itemName"), lua_isstring(L, -1)) + def.itemName = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "itemType"), lua_isstring(L, -1)) + def.itemType = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "maxStackSize"), lua_isnumber(L, -1)) + def.maxStackSize = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "weight"), lua_isnumber(L, -1)) + def.weight = (float)lua_tonumber(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "value"), lua_isnumber(L, -1)) + def.value = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "useActionName"), lua_isstring(L, -1)) + def.useActionName = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, 2, "unique"), lua_isboolean(L, -1)) + def.unique = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); + s_stubItems[itemId] = def; + return 0; + }); + lua_setfield(L, -2, "register"); + + // items.find + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + auto it = s_stubItems.find(lua_tostring(L, 1)); + if (it == s_stubItems.end()) + return 0; + lua_newtable(L); + lua_pushstring(L, it->second.itemId.c_str()); + lua_setfield(L, -2, "itemId"); + lua_pushstring(L, it->second.itemName.c_str()); + lua_setfield(L, -2, "itemName"); + lua_pushstring(L, it->second.itemType.c_str()); + lua_setfield(L, -2, "itemType"); + lua_pushinteger(L, it->second.maxStackSize); + lua_setfield(L, -2, "maxStackSize"); + lua_pushnumber(L, it->second.weight); + lua_setfield(L, -2, "weight"); + lua_pushinteger(L, it->second.value); + lua_setfield(L, -2, "value"); + lua_pushstring(L, it->second.useActionName.c_str()); + lua_setfield(L, -2, "useActionName"); + lua_pushboolean(L, it->second.unique ? 1 : 0); + lua_setfield(L, -2, "unique"); + return 1; + }); + lua_setfield(L, -2, "find"); + + // items.list + lua_pushcfunction(L, [](lua_State *L) -> int { + lua_newtable(L); + int idx = 1; + for (const auto &pair : s_stubItems) { + lua_pushstring(L, pair.first.c_str()); + lua_rawseti(L, -2, idx++); + } + return 1; + }); + lua_setfield(L, -2, "list"); + + // items.is_unique + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + auto it = s_stubItems.find(lua_tostring(L, 1)); + lua_pushboolean(L, + (it != s_stubItems.end() && it->second.unique) ? + 1 : + 0); + return 1; + }); + lua_setfield(L, -2, "is_unique"); + + lua_setfield(L, -2, "items"); + + // ecs.inventory + lua_newtable(L); + + // inventory.add + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 3 || !lua_isnumber(L, 1) || + !lua_isstring(L, 2) || !lua_isnumber(L, 3)) + return 0; + int entityId = (int)lua_tointeger(L, 1); + std::string itemId = lua_tostring(L, 2); + int count = (int)lua_tointeger(L, 3); + auto &inv = s_stubInventories[entityId]; + bool found = false; + for (auto &slot : inv) { + if (slot.itemId == itemId) { + slot.stackSize += count; + found = true; + break; + } + } + if (!found) + inv.push_back({ itemId, count }); + lua_pushboolean(L, 1); + return 1; + }); + lua_setfield(L, -2, "add"); + + // inventory.remove + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 3 || !lua_isnumber(L, 1) || + !lua_isstring(L, 2) || !lua_isnumber(L, 3)) + return 0; + int entityId = (int)lua_tointeger(L, 1); + std::string itemId = lua_tostring(L, 2); + int count = (int)lua_tointeger(L, 3); + int removed = 0; + auto it = s_stubInventories.find(entityId); + if (it != s_stubInventories.end()) { + for (auto &slot : it->second) { + if (slot.itemId == itemId) { + int rem = std::min(count, slot.stackSize); + slot.stackSize -= rem; + count -= rem; + removed += rem; + } + } + it->second.erase( + std::remove_if(it->second.begin(), it->second.end(), + [](const StubInvSlot &s) { + return s.stackSize <= 0; + }), + it->second.end()); + } + lua_pushinteger(L, removed); + return 1; + }); + lua_setfield(L, -2, "remove"); + + // inventory.has + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 2 || !lua_isnumber(L, 1) || + !lua_isstring(L, 2)) + return 0; + int entityId = (int)lua_tointeger(L, 1); + std::string itemId = lua_tostring(L, 2); + bool has = false; + auto it = s_stubInventories.find(entityId); + if (it != s_stubInventories.end()) { + for (const auto &slot : it->second) { + if (slot.itemId == itemId && slot.stackSize > 0) { + has = true; + break; + } + } + } + lua_pushboolean(L, has ? 1 : 0); + return 1; + }); + lua_setfield(L, -2, "has"); + + // inventory.count + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 2 || !lua_isnumber(L, 1) || + !lua_isstring(L, 2)) + return 0; + int entityId = (int)lua_tointeger(L, 1); + std::string itemId = lua_tostring(L, 2); + int count = 0; + auto it = s_stubInventories.find(entityId); + if (it != s_stubInventories.end()) { + for (const auto &slot : it->second) { + if (slot.itemId == itemId) + count += slot.stackSize; + } + } + lua_pushinteger(L, count); + return 1; + }); + lua_setfield(L, -2, "count"); + + // inventory.get_slots + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 1 || !lua_isnumber(L, 1)) + return 0; + int entityId = (int)lua_tointeger(L, 1); + lua_newtable(L); + auto it = s_stubInventories.find(entityId); + if (it != s_stubInventories.end()) { + int idx = 1; + for (const auto &slot : it->second) { + if (slot.stackSize <= 0) + continue; + lua_newtable(L); + lua_pushstring(L, slot.itemId.c_str()); + lua_setfield(L, -2, "itemId"); + lua_pushinteger(L, slot.stackSize); + lua_setfield(L, -2, "stackSize"); + lua_rawseti(L, -2, idx++); + } + } + return 1; + }); + lua_setfield(L, -2, "get_slots"); + + // inventory.set_slots + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 2 || !lua_isnumber(L, 1) || + !lua_istable(L, 2)) + return 0; + int entityId = (int)lua_tointeger(L, 1); + std::vector slots; + int len = (int)lua_rawlen(L, 2); + for (int i = 1; i <= len; i++) { + lua_rawgeti(L, 2, i); + if (lua_istable(L, -1)) { + StubInvSlot slot; + if (lua_getfield(L, -1, "itemId"), lua_isstring(L, -1)) + slot.itemId = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, -1, "stackSize"), lua_isnumber(L, -1)) + slot.stackSize = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + if (!slot.itemId.empty()) + slots.push_back(slot); + } + lua_pop(L, 1); + } + s_stubInventories[entityId] = slots; + return 0; + }); + lua_setfield(L, -2, "set_slots"); + + lua_setfield(L, -2, "inventory"); + + // ecs.container + lua_newtable(L); + + // container.get_state + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + std::string containerId = lua_tostring(L, 1); + lua_newtable(L); + auto it = s_stubContainerStates.find(containerId); + if (it != s_stubContainerStates.end()) { + int idx = 1; + for (const auto &slot : it->second) { + lua_newtable(L); + lua_pushstring(L, slot.itemId.c_str()); + lua_setfield(L, -2, "itemId"); + lua_pushinteger(L, slot.stackSize); + lua_setfield(L, -2, "stackSize"); + lua_rawseti(L, -2, idx++); + } + } + return 1; + }); + lua_setfield(L, -2, "get_state"); + + // container.set_state + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 2 || !lua_isstring(L, 1) || + !lua_istable(L, 2)) + return 0; + std::string containerId = lua_tostring(L, 1); + std::vector slots; + int len = (int)lua_rawlen(L, 2); + for (int i = 1; i <= len; i++) { + lua_rawgeti(L, 2, i); + if (lua_istable(L, -1)) { + StubInvSlot slot; + if (lua_getfield(L, -1, "itemId"), lua_isstring(L, -1)) + slot.itemId = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, -1, "stackSize"), lua_isnumber(L, -1)) + slot.stackSize = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + if (!slot.itemId.empty()) + slots.push_back(slot); + } + lua_pop(L, 1); + } + s_stubContainerStates[containerId] = slots; + return 0; + }); + lua_setfield(L, -2, "set_state"); + + // container.clear_state + lua_pushcfunction(L, [](lua_State *L) -> int { + if (lua_gettop(L) < 1 || !lua_isstring(L, 1)) + return 0; + s_stubContainerStates.erase(lua_tostring(L, 1)); + return 0; + }); + lua_setfield(L, -2, "clear_state"); + + lua_setfield(L, -2, "container"); + + lua_setglobal(L, "ecs"); +} + } // namespace editScene // --------------------------------------------------------------------------- diff --git a/src/features/editScene/ui/InventoryEditor.cpp b/src/features/editScene/ui/InventoryEditor.cpp index bbc8ca2..0830e48 100644 --- a/src/features/editScene/ui/InventoryEditor.cpp +++ b/src/features/editScene/ui/InventoryEditor.cpp @@ -1,4 +1,5 @@ #include "InventoryEditor.hpp" +#include "../systems/ItemRegistry.hpp" #include bool InventoryEditor::renderComponent(flecs::entity entity, @@ -35,10 +36,23 @@ bool InventoryEditor::renderComponent(flecs::entity entity, modified = true; } + // Container ID (only for containers) + if (inventory.isContainer) { + char idBuf[256]; + strncpy(idBuf, inventory.containerId.c_str(), sizeof(idBuf)); + idBuf[sizeof(idBuf) - 1] = '\0'; + if (ImGui::InputText("Container ID", idBuf, sizeof(idBuf))) { + inventory.containerId = idBuf; + modified = true; + } + } + ImGui::Separator(); ImGui::Text("Contents (%d items, %.1f weight)", inventory.countItems(), inventory.totalWeight); + ItemRegistry *registry = ItemRegistry::getSingletonPtr(); + // List slots for (int i = 0; i < (int)inventory.slots.size(); i++) { auto &slot = inventory.slots[i]; @@ -46,8 +60,14 @@ bool InventoryEditor::renderComponent(flecs::entity entity, continue; ImGui::PushID(i); - ImGui::Text("[%d] %s x%d (%s)", i, slot.itemName.c_str(), - slot.stackSize, slot.itemType.c_str()); + std::string displayName = slot.itemId; + if (registry) { + auto *def = registry->findDefinition(slot.itemId); + if (def) + displayName = def->itemName; + } + ImGui::Text("[%d] %s x%d", i, displayName.c_str(), + slot.stackSize); ImGui::SameLine(); if (ImGui::SmallButton("X")) { slot.clear(); diff --git a/src/features/editScene/ui/ItemEditor.cpp b/src/features/editScene/ui/ItemEditor.cpp index 6e69feb..bfdf275 100644 --- a/src/features/editScene/ui/ItemEditor.cpp +++ b/src/features/editScene/ui/ItemEditor.cpp @@ -1,5 +1,5 @@ #include "ItemEditor.hpp" -#include "../components/ActionDatabase.hpp" +#include "../systems/ItemRegistry.hpp" #include bool ItemEditor::renderComponent(flecs::entity entity, ItemComponent &item) @@ -8,27 +8,9 @@ bool ItemEditor::renderComponent(flecs::entity entity, ItemComponent &item) bool modified = false; ImGui::PushID("Item"); - ImGui::Text("Item Settings"); + ImGui::Text("Item Reference"); 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)); @@ -38,6 +20,54 @@ bool ItemEditor::renderComponent(flecs::entity entity, ItemComponent &item) modified = true; } + // Pick from registry dropdown + ItemRegistry *registry = ItemRegistry::getSingletonPtr(); + if (registry && !registry->getDefinitions().empty()) { + static int selectedRegItem = -1; + std::vector names; + std::vector ids; + int currentIdx = -1; + int idx = 0; + for (const auto &pair : registry->getDefinitions()) { + names.push_back(pair.second.itemName.c_str()); + ids.push_back(pair.second.itemId); + if (pair.second.itemId == item.itemId) + currentIdx = idx; + idx++; + } + if (currentIdx >= 0) + selectedRegItem = currentIdx; + if (selectedRegItem >= (int)names.size()) + selectedRegItem = 0; + ImGui::SameLine(); + if (ImGui::Combo("##itemIdSelect", &selectedRegItem, + names.data(), (int)names.size())) { + item.itemId = ids[selectedRegItem]; + modified = true; + } + } + + // Registry info display + if (!item.itemId.empty() && registry) { + auto *def = registry->findDefinition(item.itemId); + if (def) { + ImGui::Text("Name: %s", def->itemName.c_str()); + ImGui::Text("Type: %s", def->itemType.c_str()); + ImGui::Text("Max Stack: %d", def->maxStackSize); + ImGui::Text("Weight: %.2f", def->weight); + ImGui::Text("Value: %d", def->value); + if (!def->useActionName.empty()) + ImGui::Text("Use Action: %s", def->useActionName.c_str()); + if (def->unique) + ImGui::TextColored(ImVec4(1, 0.5f, 0, 1), "UNIQUE ITEM"); + } else { + ImGui::TextColored(ImVec4(1, 0, 0, 1), + "Unknown item ID: %s", item.itemId.c_str()); + } + } + + ImGui::Separator(); + // Stack size int stackSize = item.stackSize; if (ImGui::DragInt("Stack Size", &stackSize, 1, 1, 999)) { @@ -47,78 +77,6 @@ bool ItemEditor::renderComponent(flecs::entity entity, ItemComponent &item) 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 singleton - ActionDatabase *db = ActionDatabase::getSingletonPtr(); - - 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; }