Item registry

This commit is contained in:
2026-05-14 02:28:33 +03:00
parent eb0d05a577
commit 5bb20d416d
24 changed files with 2396 additions and 372 deletions
+22
View File
@@ -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
+8
View File
@@ -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<CharacterClassSystem>(
@@ -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();
+18 -42
View File
@@ -8,41 +8,33 @@
#include <string>
#include <cstdint>
#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;
}
}
};
+8 -36
View File
@@ -4,54 +4,26 @@
#include <Ogre.h>
#include <string>
#include <vector>
/**
* 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)
{
}
};
+282
View File
@@ -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.
+10 -32
View File
@@ -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 ---
+392
View File
@@ -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 <OgreLogManager.h>
#include <flecs.h>
static flecs::world getWorld(lua_State *L)
{
lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld");
OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered");
flecs::world *world =
static_cast<flecs::world *>(lua_touserdata(L, -1));
lua_pop(L, 1);
return *world;
}
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<InventoryComponent>()) {
auto &inv = e.get_mut<InventoryComponent>();
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<InventoryComponent>()) {
auto &inv = e.get_mut<InventoryComponent>();
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<InventoryComponent>())
has = e.get<InventoryComponent>().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<InventoryComponent>())
count = e.get<InventoryComponent>().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<InventoryComponent>()) {
auto &inv = e.get<InventoryComponent>();
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<InventoryComponent>())
return 0;
auto &inv = e.get_mut<InventoryComponent>();
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<ContainerStateRegistry::ContainerSlot> 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
+14
View File
@@ -0,0 +1,14 @@
#ifndef EDITSCENE_LUA_ITEM_API_HPP
#define EDITSCENE_LUA_ITEM_API_HPP
#pragma once
#include <lua.hpp>
namespace editScene
{
void registerLuaItemApi(lua_State *L);
} // namespace editScene
#endif // EDITSCENE_LUA_ITEM_API_HPP
@@ -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<ItemComponent>()) {
// Show "E - Pick up [ItemName]" for items
auto &item = targetEntity.get<ItemComponent>();
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;
}
}
}
@@ -786,70 +786,28 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
if (!itemSystem || !e.has<InventoryComponent>())
return Status::failure;
// Parse params: "itemId,itemName,itemType,count,weight,value"
Ogre::String itemId = node.name;
Ogre::String itemName = node.name;
Ogre::String itemType = "misc";
// 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<Ogre::String> parts;
const char *s = node.params.c_str();
const char *start = s;
while (*s) {
if (*s == ',') {
parts.push_back(Ogre::String(
start,
static_cast<size_t>(
s - start)));
start = s + 1;
}
s++;
}
if (s > start)
parts.push_back(Ogre::String(
start, static_cast<size_t>(
s - start)));
if (parts.size() >= 1)
itemName = parts[0];
if (parts.size() >= 2)
itemType = parts[1];
if (parts.size() >= 3) {
char *end = nullptr;
long val = strtol(parts[2].c_str(),
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<int>(val);
}
if (parts.size() >= 4) {
char *end = nullptr;
float val =
strtof(parts[3].c_str(), &end);
if (end != parts[3].c_str() &&
*end == '\0' && val >= 0.0f)
weight = val;
}
if (parts.size() >= 5) {
char *end = nullptr;
long val = strtol(parts[4].c_str(),
&end, 10);
if (end != parts[4].c_str() &&
*end == '\0' && val >= 0)
value = static_cast<int>(val);
}
if (end != node.params.c_str() &&
*end == '\0' && val > 0)
count = static_cast<int>(val);
}
itemSystem->addItemToInventory(e, itemId, itemName,
itemType, count, weight,
value);
std::cout << "[BT] addItemToInventory: added "
<< itemName << " x" << count << std::endl;
return Status::success;
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;
}
@@ -0,0 +1,125 @@
#include "ContainerStateRegistry.hpp"
#include <OgreLogManager.h>
#include <fstream>
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<ContainerSlot> &slots)
{
ContainerState &state = m_states[containerId];
state.containerId = containerId;
state.slots = slots;
autoSave();
}
std::vector<ContainerStateRegistry::ContainerSlot>
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);
}
@@ -0,0 +1,61 @@
#ifndef EDITSCENE_CONTAINERSTATEREGISTRY_HPP
#define EDITSCENE_CONTAINERSTATEREGISTRY_HPP
#pragma once
#include <nlohmann/json.hpp>
#include <string>
#include <unordered_map>
#include <vector>
/**
* 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<ContainerSlot> slots;
};
void loadState(const std::string &containerId,
const std::vector<ContainerSlot> &slots);
std::vector<ContainerSlot> 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<std::string, ContainerState> m_states;
mutable std::string m_lastError;
std::string m_autoSavePath;
};
#endif // EDITSCENE_CONTAINERSTATEREGISTRY_HPP
@@ -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();
}
@@ -316,6 +316,9 @@ private:
bool m_showCharacterRegistry = false;
CharacterRegistry m_characterRegistry;
// Item registry
bool m_showItemRegistry = false;
// Queries
flecs::query<EntityNameComponent> m_nameQuery;
@@ -0,0 +1,371 @@
#include "ItemRegistry.hpp"
#include <OgreLogManager.h>
#include <fstream>
#include <imgui.h>
#include <algorithm>
/* ===================================================================== */
/* 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<int64_t>();
}
if (defJson.contains("floatColumns")) {
for (auto &[key, val] : defJson["floatColumns"].items())
d.floatColumns[key] = val.get<double>();
}
if (defJson.contains("stringColumns")) {
for (auto &[key, val] : defJson["stringColumns"].items())
d.stringColumns[key] = val.get<std::string>();
}
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();
}
@@ -0,0 +1,137 @@
#ifndef EDITSCENE_ITEMREGISTRY_HPP
#define EDITSCENE_ITEMREGISTRY_HPP
#pragma once
#include <Ogre.h>
#include <nlohmann/json.hpp>
#include <string>
#include <unordered_map>
#include <vector>
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<std::string, int64_t> intColumns;
std::unordered_map<std::string, double> floatColumns;
std::unordered_map<std::string, std::string> 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<std::string, ItemDefinition> &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<ColumnDef> &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<std::string, ItemDefinition> m_definitions;
std::vector<ColumnDef> 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
+59 -54
View File
@@ -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<InventoryComponent>())
return false;
if (itemId.empty() || stackSize <= 0)
return false;
auto &inv = inventoryEntity.get_mut<InventoryComponent>();
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<ItemComponent>();
// 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<InventoryComponent>();
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<TransformComponent>()) {
auto &trans = itemEntity.get_mut<TransformComponent>();
@@ -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<InventoryComponent>())
@@ -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<InventoryComponent>())
@@ -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<InventoryComponent>())
return false;
return inventoryEntity.get<InventoryComponent>().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<InventoryComponent>().hasItem(foundId);
}
int ItemSystem::countItem(flecs::entity inventoryEntity,
const Ogre::String &itemId) const
const std::string &itemId) const
{
if (!inventoryEntity.is_alive() ||
!inventoryEntity.has<InventoryComponent>())
@@ -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>(
ItemComponent(slot.itemName, slot.itemType));
auto &item = itemEntity.get_mut<ItemComponent>();
item.itemId = slot.itemId;
item.stackSize = dropCount;
item.weight = slot.weight;
item.value = slot.value;
item.useActionName = slot.useActionName;
itemEntity.set<ItemComponent>(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<TransformComponent>(trans);
EntityNameComponent nameComp;
nameComp.name = slot.itemName + "_dropped";
nameComp.name = itemName + "_dropped";
itemEntity.set<EntityNameComponent>(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<ItemComponent>().itemName);
ItemRegistry::getSingleton().getItemName(
itemEntity.get<ItemComponent>().itemId));
}
return result;
}
+8 -15
View File
@@ -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,
@@ -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<ItemComponent>();
nlohmann::json json;
json["itemName"] = item.itemName;
json["itemType"] = item.itemType;
json["itemId"] = item.itemId;
json["stackSize"] = item.stackSize;
json["maxStackSize"] = item.maxStackSize;
json["weight"] = item.weight;
json["value"] = item.value;
json["useActionName"] = item.useActionName;
return json;
}
@@ -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<ItemComponent>(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<InventoryComponent>(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<InventoryComponent>(inv);
}
}
}
@@ -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");
@@ -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 <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <vector>
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
#include "ogre_stub.h"
// Include Lua
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
// 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;
}
@@ -20,6 +20,7 @@
#include <unordered_map>
#include <vector>
#include <map>
#include <algorithm>
// ---------------------------------------------------------------------------
// 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<std::string, StubItemDef> s_stubItems;
struct StubInvSlot {
std::string itemId;
int stackSize = 0;
};
static std::unordered_map<int, std::vector<StubInvSlot> > s_stubInventories;
static std::unordered_map<std::string, std::vector<StubInvSlot> > 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<StubInvSlot> 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<StubInvSlot> 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
// ---------------------------------------------------------------------------
+22 -2
View File
@@ -1,4 +1,5 @@
#include "InventoryEditor.hpp"
#include "../systems/ItemRegistry.hpp"
#include <imgui.h>
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();
+50 -92
View File
@@ -1,5 +1,5 @@
#include "ItemEditor.hpp"
#include "../components/ActionDatabase.hpp"
#include "../systems/ItemRegistry.hpp"
#include <imgui.h>
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<const char *> names;
std::vector<std::string> 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<const char *> availableNames;
std::vector<Ogre::String> availableNamesStorage;
for (const auto &action : db->actions) {
availableNamesStorage.push_back(action.name);
availableNames.push_back(
availableNamesStorage.back().c_str());
}
if (!availableNames.empty()) {
if (selectedAction >= (int)availableNames.size())
selectedAction = 0;
ImGui::SameLine();
if (ImGui::Combo("##useActionSelect", &selectedAction,
availableNames.data(),
(int)availableNames.size())) {
}
ImGui::SameLine();
if (ImGui::Button("Set")) {
if (selectedAction >= 0 &&
selectedAction <
(int)availableNames.size()) {
item.useActionName =
availableNamesStorage
[selectedAction];
modified = true;
}
}
}
}
ImGui::PopID();
return modified;
}