Item registry
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user