Updated APIs and tests

This commit is contained in:
2026-05-14 13:32:32 +03:00
parent 5bb20d416d
commit 8630bfcf18
22 changed files with 3228 additions and 90 deletions
+34
View File
@@ -38,6 +38,7 @@ set(EDITSCENE_SOURCES
systems/PauseMenuSystem.cpp
systems/ItemRegistry.cpp
systems/ContainerStateRegistry.cpp
systems/ItemStateRegistry.cpp
systems/PlayerControllerSystem.cpp
systems/CharacterSlotSystem.cpp
systems/CharacterRegistry.cpp
@@ -213,6 +214,7 @@ set(EDITSCENE_HEADERS
systems/PauseMenuSystem.hpp
systems/ItemRegistry.hpp
systems/ContainerStateRegistry.hpp
systems/ItemStateRegistry.hpp
systems/PlayerControllerSystem.hpp
systems/EditorUISystem.hpp
systems/CellGridSystem.hpp
@@ -533,6 +535,38 @@ target_include_directories(character_lua_test PRIVATE
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: Dialogue Lua API
add_executable(dialogue_lua_test
tests/dialogue_lua_test.cpp
tests/lua_test_stubs.cpp
)
target_link_libraries(dialogue_lua_test
lua
)
target_include_directories(dialogue_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: Character Class Lua API
add_executable(character_class_lua_test
tests/character_class_lua_test.cpp
tests/lua_test_stubs.cpp
)
target_link_libraries(character_class_lua_test
lua
)
target_include_directories(character_class_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Copy local resources (materials, etc.)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources"
+3
View File
@@ -33,6 +33,7 @@
#include "systems/DialogueSystem.hpp"
#include "systems/ItemRegistry.hpp"
#include "systems/ContainerStateRegistry.hpp"
#include "systems/ItemStateRegistry.hpp"
#include "systems/CharacterClassSystem.hpp"
#include "systems/PregnancySystem.hpp"
#include "components/CharacterClassDatabase.hpp"
@@ -469,6 +470,8 @@ void EditorApp::setup()
PauseMenuSystem::getInstance().init(this);
static ItemRegistry s_itemRegistry;
ItemRegistry::getSingleton().initialize();
ItemStateRegistry::getInstance().loadFromFile(
"item_state.json");
ContainerStateRegistry::getInstance().loadFromFile(
"container_state.json");
+15 -1
View File
@@ -8,9 +8,14 @@
/**
* Item reference component.
*
* Attached to a world entity that represents a pickable item.
* Attached to a world entity that represents a pickable / interactable item.
* All item properties (name, type, weight, etc.) are stored in
* the ItemRegistry singleton and looked up by itemId.
*
* action: Optional GOAP action name to execute on interact (E key).
* If empty, the item is picked up into inventory.
* instanceId: Optional unique ID for global state tracking (save/load).
* disabled: Set to true when the item has been picked up / consumed.
*/
struct ItemComponent {
// Registry key for this item definition
@@ -19,6 +24,15 @@ struct ItemComponent {
// Stack size for this world entity
int stackSize = 1;
// Optional action to execute on interact (instead of pickup)
Ogre::String action;
// Unique instance ID for global state tracking
Ogre::String instanceId;
// True when item has been picked up or consumed
bool disabled = false;
ItemComponent() = default;
explicit ItemComponent(const Ogre::String &id, int stack = 1)
@@ -0,0 +1,262 @@
-- =============================================================================
-- Character Class Lua API Examples
-- =============================================================================
-- This file demonstrates how to use the ecs.character_class API for
-- querying character class definitions and managing per-entity character
-- stats, skills, needs, and resource pools.
--
-- The character class system provides:
-- - Database queries for class definitions, stats, skills, and needs
-- - Per-entity runtime stats (level, XP, stats, skills, needs)
-- - Resource pool management (health, mana, stamina, etc.)
-- =============================================================================
-- =============================================================================
-- Database Queries
-- =============================================================================
-- List all registered class names
local class_names = ecs.character_class.get_class_names()
print("Registered classes (" .. #class_names .. "):")
for _, name in ipairs(class_names) do
print(" - " .. name)
end
-- List all stat names
local stat_names = ecs.character_class.get_stat_names()
print("Stats (" .. #stat_names .. "):")
for _, name in ipairs(stat_names) do
print(" - " .. name)
end
-- List all skill names
local skill_names = ecs.character_class.get_skill_names()
print("Skills (" .. #skill_names .. "):")
for _, name in ipairs(skill_names) do
print(" - " .. name)
end
-- List all need names
local need_names = ecs.character_class.get_need_names()
print("Needs (" .. #need_names .. "):")
for _, name in ipairs(need_names) do
print(" - " .. name)
end
-- =============================================================================
-- Querying Class Definitions
-- =============================================================================
-- Get a specific class definition
local warrior_class = ecs.character_class.get_class("warrior")
if warrior_class then
print("Warrior class:")
print(" Name: " .. warrior_class.name)
print(" Description: " .. warrior_class.description)
print(" Primary stats (" .. #warrior_class.primary_stats .. "):")
for _, stat in ipairs(warrior_class.primary_stats) do
print(" - " .. stat)
end
else
print("Warrior class not found (stub database may be empty)")
end
-- =============================================================================
-- Stat Kind Queries
-- =============================================================================
-- Check the kind of a stat (attribute vs resource_pool)
local strength_kind = ecs.character_class.get_stat_kind("strength")
print("Strength stat kind: " .. strength_kind)
local health_kind = ecs.character_class.get_stat_kind("health")
print("Health stat kind: " .. health_kind)
-- Unknown stat returns "unknown"
local unknown_kind = ecs.character_class.get_stat_kind("nonexistent")
print("Unknown stat kind: " .. unknown_kind)
-- =============================================================================
-- Per-Entity Character Stats
-- =============================================================================
-- Create a player entity with character identity
local player = ecs.create_entity()
ecs.set_entity_name(player, "hero")
ecs.set_component(player, "CharacterIdentity", {
registryId = 1
})
print("Created player entity (ID: " .. player .. ")")
-- Get character level
local level = ecs.character_class.get_level(player)
print("Player level: " .. level)
-- Get current XP
local xp = ecs.character_class.get_xp(player)
print("Player XP: " .. xp)
-- Add XP to the player
local xp_added = ecs.character_class.add_xp(player, 150)
print("XP added: " .. tostring(xp_added))
-- Verify XP was added
local new_xp = ecs.character_class.get_xp(player)
print("Player XP after adding: " .. new_xp)
-- =============================================================================
-- Stat, Skill, and Need Queries
-- =============================================================================
-- Get a specific stat value
local strength = ecs.character_class.get_stat(player, "strength")
print("Player strength: " .. strength)
-- Get a specific skill value
local swordsmanship = ecs.character_class.get_skill(player, "swordsmanship")
print("Player swordsmanship: " .. swordsmanship)
-- Get a specific need value
local hunger = ecs.character_class.get_need(player, "hunger")
print("Player hunger: " .. hunger)
-- Get available attribute/skill points
local available_points = ecs.character_class.get_available_points(player)
print("Available points: " .. available_points)
-- =============================================================================
-- Setting Needs
-- =============================================================================
-- Set a need value (e.g., after eating)
ecs.character_class.set_need(player, "hunger", 0)
print("Set hunger to 0")
-- Verify the change
local new_hunger = ecs.character_class.get_need(player, "hunger")
print("Hunger after setting: " .. new_hunger)
-- =============================================================================
-- Resource Pool Management
-- =============================================================================
-- Get current pool value (e.g., health)
local current_health = ecs.character_class.get_pool_current(player, "health")
print("Current health: " .. current_health)
-- Get maximum pool value
local max_health = ecs.character_class.get_pool_max(player, "health")
print("Max health: " .. max_health)
-- Set current pool value (e.g., after taking damage)
local pool_set = ecs.character_class.set_pool_current(player, "health", 75)
print("Health set to 75: " .. tostring(pool_set))
-- Verify the change
local new_health = ecs.character_class.get_pool_current(player, "health")
print("Health after setting: " .. new_health)
-- =============================================================================
-- Practical: Character Level Up
-- =============================================================================
function level_up(entity, xp_gained)
print("=== Level Up ===")
-- Add XP
ecs.character_class.add_xp(entity, xp_gained)
-- Get updated level
local new_level = ecs.character_class.get_level(entity)
print("New level: " .. new_level)
-- Get available points
local points = ecs.character_class.get_available_points(entity)
print("Points to spend: " .. points)
-- Increase a stat
local current_str = ecs.character_class.get_stat(entity, "strength")
print("Strength was: " .. current_str)
-- Note: Stats are typically increased via the character system,
-- not directly through this API. This example shows querying.
-- Restore health on level up
local max_hp = ecs.character_class.get_pool_max(entity, "health")
ecs.character_class.set_pool_current(entity, "health", max_hp)
print("Health restored to max: " .. max_hp)
print("=== Level Up Complete ===")
end
level_up(player, 500)
-- =============================================================================
-- Practical: Character Status Report
-- =============================================================================
function print_character_status(entity)
print("=== Character Status ===")
print("Level: " .. ecs.character_class.get_level(entity))
print("XP: " .. ecs.character_class.get_xp(entity))
print("Available Points: " .. ecs.character_class.get_available_points(entity))
print("Stats:")
for _, name in ipairs(stat_names) do
local val = ecs.character_class.get_stat(entity, name)
local kind = ecs.character_class.get_stat_kind(name)
if kind == "resource_pool" then
local current = ecs.character_class.get_pool_current(entity, name)
local max = ecs.character_class.get_pool_max(entity, name)
print(" " .. name .. ": " .. current .. "/" .. max)
else
print(" " .. name .. ": " .. val)
end
end
print("Skills:")
for _, name in ipairs(skill_names) do
local val = ecs.character_class.get_skill(entity, name)
print(" " .. name .. ": " .. val)
end
print("Needs:")
for _, name in ipairs(need_names) do
local val = ecs.character_class.get_need(entity, name)
print(" " .. name .. ": " .. val)
end
print("=== End Status ===")
end
print_character_status(player)
-- =============================================================================
-- API Reference
-- =============================================================================
--
-- Database Queries:
-- ecs.character_class.get_class_names() -> table of strings
-- ecs.character_class.get_stat_names() -> table of strings
-- ecs.character_class.get_skill_names() -> table of strings
-- ecs.character_class.get_need_names() -> table of strings
-- ecs.character_class.get_class(name) -> table or nil
-- Returns { name, description, primary_stats }
-- ecs.character_class.get_stat_kind(name) -> string
-- Returns "attribute", "resource_pool", or "unknown"
--
-- Per-Entity Runtime API:
-- ecs.character_class.get_level(entity_id) -> int
-- ecs.character_class.get_xp(entity_id) -> int
-- ecs.character_class.add_xp(entity_id, amount) -> bool
-- ecs.character_class.get_stat(entity_id, stat_name) -> int
-- ecs.character_class.get_skill(entity_id, skill_name) -> int
-- ecs.character_class.get_need(entity_id, need_name) -> int
-- ecs.character_class.get_available_points(entity_id) -> int
-- ecs.character_class.set_need(entity_id, need_name, value) -> nil
-- ecs.character_class.get_pool_current(entity_id, pool_name) -> int
-- ecs.character_class.get_pool_max(entity_id, pool_name) -> int
-- ecs.character_class.set_pool_current(entity_id, pool_name, value) -> bool
-- =============================================================================
print("Character class examples completed successfully!")
@@ -0,0 +1,284 @@
-- =============================================================================
-- Character Lua API Examples
-- =============================================================================
-- This file demonstrates how to use the ecs.character API for managing
-- character records in the CharacterRegistry.
--
-- The character system provides:
-- - Character creation, deletion, and lookup
-- - Spawning/despawning characters as ECS entities
-- - Pregnancy management (conceive, abort, check progress)
-- - Lineage tracking (parents, children, create_child)
-- - Name management
-- =============================================================================
-- =============================================================================
-- Character Creation
-- =============================================================================
-- Create a character with first name, last name, template path, and persistence
local hero = ecs.character.create("Arthur", "Pendragon", "characters/knight", true)
print("Created hero character (ID: " .. hero .. ")")
-- Create a non-persistent character (e.g., a temporary NPC)
local npc = ecs.character.create("Merlin", "the Wise", "characters/wizard", false)
print("Created NPC character (ID: " .. npc .. ")")
-- Create a character without a template path
local villager = ecs.character.create("John", "Smith", "", true)
print("Created villager character (ID: " .. villager .. ")")
-- =============================================================================
-- Character Lookup
-- =============================================================================
-- Find a character by ID
local found = ecs.character.find(hero)
if found then
print("Found character:")
print(" ID: " .. found.id)
print(" Name: " .. found.firstName .. " " .. found.lastName)
print(" Class: " .. found.className)
print(" Level: " .. found.level)
print(" Age: " .. found.ageYears)
print(" Sex: " .. found.sex)
print(" Persistent: " .. tostring(found.persistent))
print(" Pregnant by: " .. found.pregnantByFatherId)
print(" Pregnancy progress: " .. found.pregnancyProgress)
print(" Pregnancy max progress: " .. found.pregnancyMaxProgress)
end
-- Find a non-existent character returns nil
local missing = ecs.character.find(99999)
print("Non-existent character: " .. tostring(missing))
-- =============================================================================
-- Listing All Characters
-- =============================================================================
-- Get all character IDs
local all_chars = ecs.character.get_all()
print("All characters (" .. #all_chars .. "):")
for _, id in ipairs(all_chars) do
local c = ecs.character.find(id)
if c then
print(" [" .. id .. "] " .. c.firstName .. " " .. c.lastName)
end
end
-- =============================================================================
-- Character Name Management
-- =============================================================================
-- Change a character's name
ecs.character.set_name(hero, "Arthur", "the Great")
print("Renamed hero")
-- Verify the name change
local renamed = ecs.character.find(hero)
print("New name: " .. renamed.firstName .. " " .. renamed.lastName)
-- =============================================================================
-- Spawning and Despawning
-- =============================================================================
-- Spawn a character as an ECS entity (returns entity ID)
local entity_id = ecs.character.spawn(hero)
if entity_id then
print("Spawned hero as entity (ID: " .. entity_id .. ")")
else
print("Failed to spawn hero")
end
-- Check if a character is spawned
local is_spawned = ecs.character.is_spawned(hero)
print("Hero is spawned: " .. tostring(is_spawned))
-- Despawn a character
local despawned = ecs.character.despawn(hero)
print("Hero despawned: " .. tostring(despawned))
-- Verify despawn
local still_spawned = ecs.character.is_spawned(hero)
print("Hero still spawned: " .. tostring(still_spawned))
-- =============================================================================
-- Pregnancy Management
-- =============================================================================
-- Create two characters for pregnancy demo
local mother = ecs.character.create("Guinevere", "Pendragon", "", true)
local father = ecs.character.create("Lancelot", "du Lac", "", true)
print("Created mother (ID: " .. mother .. ") and father (ID: " .. father .. ")")
-- Conceive a child
local conceived = ecs.character.conceive(mother, father)
print("Conceived: " .. tostring(conceived))
-- Check if pregnant
local pregnant = ecs.character.is_pregnant(mother)
print("Is pregnant: " .. tostring(pregnant))
-- Get pregnancy progress
local progress = ecs.character.get_pregnancy_progress(mother)
if progress then
print("Pregnancy progress:")
print(" Progress: " .. progress.progress)
print(" Max progress: " .. progress.maxProgress)
print(" Ratio: " .. progress.ratio)
end
-- Abort pregnancy
ecs.character.abort_pregnancy(mother)
print("Pregnancy aborted")
-- Verify abortion
local still_pregnant = ecs.character.is_pregnant(mother)
print("Still pregnant: " .. tostring(still_pregnant))
-- =============================================================================
-- Lineage: Creating Children
-- =============================================================================
-- Create a child from two parents
local child = ecs.character.create_child(mother, father)
print("Created child (ID: " .. child .. ")")
-- Get the child's info
local child_info = ecs.character.find(child)
print("Child name: " .. child_info.firstName .. " " .. child_info.lastName)
-- Get parents of the child
local parents = ecs.character.get_parents(child)
print("Parents of child (" .. #parents .. "):")
for _, parent_id in ipairs(parents) do
local p = ecs.character.find(parent_id)
if p then
print(" [" .. parent_id .. "] " .. p.firstName .. " " .. p.lastName)
end
end
-- Get children of a parent
local children = ecs.character.get_children(mother)
print("Children of mother (" .. #children .. "):")
for _, child_id in ipairs(children) do
local c = ecs.character.find(child_id)
if c then
print(" [" .. child_id .. "] " .. c.firstName .. " " .. c.lastName)
end
end
-- =============================================================================
-- Practical: Family Tree
-- =============================================================================
function print_family_tree(character_id, indent)
indent = indent or ""
local c = ecs.character.find(character_id)
if not c then
print(indent .. "[Unknown character]")
return
end
print(indent .. c.firstName .. " " .. c.lastName .. " (ID: " .. c.id .. ")")
-- Print children
local kids = ecs.character.get_children(character_id)
for _, kid_id in ipairs(kids) do
print_family_tree(kid_id, indent .. " ")
end
end
print("=== Family Tree ===")
print_family_tree(mother)
-- =============================================================================
-- Practical: Character Lifecycle
-- =============================================================================
function character_lifecycle_demo()
print("=== Character Lifecycle Demo ===")
-- 1. Create
local sim = ecs.character.create("Sim", "One", "", true)
print("1. Created character: " .. sim)
-- 2. Spawn
local eid = ecs.character.spawn(sim)
print("2. Spawned as entity: " .. tostring(eid))
-- 3. Rename
ecs.character.set_name(sim, "Simantha", "One")
print("3. Renamed to: Simantha One")
-- 4. Despawn
ecs.character.despawn(sim)
print("4. Despawned")
-- 5. Delete
ecs.character.delete(sim)
print("5. Deleted")
-- Verify deletion
local gone = ecs.character.find(sim)
print("6. After deletion: " .. tostring(gone))
print("=== Lifecycle Demo Complete ===")
end
character_lifecycle_demo()
-- =============================================================================
-- API Reference
-- =============================================================================
--
-- ecs.character.create(firstName, lastName, templatePath, persistent) -> int
-- Creates a new character record. Returns the character ID.
--
-- ecs.character.delete(id) -> nil
-- Deletes a character record.
--
-- ecs.character.find(id) -> table or nil
-- Returns character info: id, firstName, lastName, className, level,
-- ageYears, sex, persistent, pregnantByFatherId, pregnancyProgress,
-- pregnancyMaxProgress.
--
-- ecs.character.get_all() -> table of ints
-- Returns all character IDs.
--
-- ecs.character.set_name(id, firstName, lastName) -> nil
-- Updates a character's name.
--
-- ecs.character.spawn(id) -> int or nil
-- Spawns the character as an ECS entity. Returns the entity ID.
--
-- ecs.character.despawn(id) -> bool
-- Despawns the character's entity.
--
-- ecs.character.is_spawned(id) -> bool
-- Returns true if the character is currently spawned.
--
-- ecs.character.conceive(motherId, fatherId) -> bool
-- Initiates pregnancy for the mother.
--
-- ecs.character.abort_pregnancy(motherId) -> nil
-- Aborts the mother's pregnancy.
--
-- ecs.character.is_pregnant(motherId) -> bool
-- Returns true if the mother is pregnant.
--
-- ecs.character.get_pregnancy_progress(motherId) -> table or nil
-- Returns { progress, maxProgress, ratio } or nil if not pregnant.
--
-- ecs.character.create_child(parentA, parentB) -> int
-- Creates a child character from two parents.
--
-- ecs.character.get_parents(childId) -> table of ints
-- Returns the parent IDs of a child.
--
-- ecs.character.get_children(parentId) -> table of ints
-- Returns the child IDs of a parent.
-- =============================================================================
print("Character examples completed successfully!")
@@ -0,0 +1,199 @@
-- =============================================================================
-- Container State Lua API Examples
-- =============================================================================
-- This file demonstrates how to manage persistent container states using
-- the ecs.container API. Container states are stored in the
-- ContainerStateRegistry singleton, separate from entity inventories.
--
-- Use cases:
-- - Persistent chests that remember their contents across sessions
-- - Shop inventories that reset on a timer
-- - Quest containers that change state based on story progress
-- - Lootable containers that can be emptied permanently
-- =============================================================================
-- =============================================================================
-- Setting Container State
-- =============================================================================
-- Define the contents of a treasure chest
ecs.container.set_state("treasure_chest_001", {
{ itemId = "sword_iron", stackSize = 1 },
{ itemId = "potion_health", stackSize = 3 },
{ itemId = "gold_coin", stackSize = 100 }
})
print("Set state for treasure_chest_001")
-- Define a shop inventory
ecs.container.set_state("blacksmith_shop", {
{ itemId = "sword_iron", stackSize = 3 },
{ itemId = "bow_wood", stackSize = 2 },
{ itemId = "arrow", stackSize = 50 },
{ itemId = "potion_health", stackSize = 5 }
})
print("Set state for blacksmith_shop")
-- =============================================================================
-- Getting Container State
-- =============================================================================
-- Retrieve the contents of a container
local chest_contents = ecs.container.get_state("treasure_chest_001")
print("Treasure chest contents (" .. #chest_contents .. " items):")
for i, slot in ipairs(chest_contents) do
print(" [" .. i .. "] " .. slot.itemId .. " x" .. slot.stackSize)
end
-- =============================================================================
-- Updating Container State
-- =============================================================================
-- Simulate looting the chest: remove items and update state
local function loot_container(container_id, item_id, count)
local contents = ecs.container.get_state(container_id)
local remaining = count
-- Build new contents minus the looted items
local new_contents = {}
for _, slot in ipairs(contents) do
if slot.itemId == item_id and remaining > 0 then
local to_remove = math.min(remaining, slot.stackSize)
slot.stackSize = slot.stackSize - to_remove
remaining = remaining - to_remove
end
if slot.stackSize > 0 then
table.insert(new_contents, slot)
end
end
ecs.container.set_state(container_id, new_contents)
print("Looted " .. (count - remaining) .. " " .. item_id .. " from " .. container_id)
return count - remaining
end
-- Player loots 2 health potions from the chest
loot_container("treasure_chest_001", "potion_health", 2)
-- Check what's left
local remaining = ecs.container.get_state("treasure_chest_001")
print("Remaining in treasure_chest_001:")
for i, slot in ipairs(remaining) do
print(" [" .. i .. "] " .. slot.itemId .. " x" .. slot.stackSize)
end
-- =============================================================================
-- Clearing Container State
-- =============================================================================
-- Clear a container's state (e.g., after it's been fully looted)
ecs.container.clear_state("treasure_chest_001")
print("Cleared state for treasure_chest_001")
-- Verify it's empty
local empty = ecs.container.get_state("treasure_chest_001")
print("After clear, contents count: " .. #empty)
-- =============================================================================
-- Practical: Shop Restocking
-- =============================================================================
-- Restock a shop's inventory (reset to initial state)
function restock_shop(shop_id)
local stock = {
blacksmith_shop = {
{ itemId = "sword_iron", stackSize = 3 },
{ itemId = "bow_wood", stackSize = 2 },
{ itemId = "arrow", stackSize = 50 },
{ itemId = "potion_health", stackSize = 5 }
},
alchemist_shop = {
{ itemId = "potion_health", stackSize = 10 },
{ itemId = "potion_stamina", stackSize = 10 }
}
}
if stock[shop_id] then
ecs.container.set_state(shop_id, stock[shop_id])
print("Restocked " .. shop_id)
else
print("Unknown shop: " .. shop_id)
end
end
-- Simulate a purchase: remove items from shop
local function buy_from_shop(shop_id, item_id, count)
local contents = ecs.container.get_state(shop_id)
local available = 0
for _, slot in ipairs(contents) do
if slot.itemId == item_id then
available = slot.stackSize
break
end
end
if available >= count then
loot_container(shop_id, item_id, count)
print("Purchased " .. count .. " " .. item_id .. " from " .. shop_id)
return true
else
print("Not enough stock in " .. shop_id .. " (have " .. available .. ", need " .. count .. ")")
return false
end
end
-- Player buys from blacksmith
buy_from_shop("blacksmith_shop", "sword_iron", 1)
buy_from_shop("blacksmith_shop", "arrow", 10)
-- Restock the shop
restock_shop("blacksmith_shop")
-- =============================================================================
-- Practical: Quest Container with Conditional State
-- =============================================================================
-- A quest container that changes based on story progress
function setup_quest_container(quest_stage)
if quest_stage == "not_started" then
ecs.container.set_state("ancient_tomb", {
{ itemId = "potion_health", stackSize = 2 },
{ itemId = "gold_coin", stackSize = 50 }
})
elseif quest_stage == "in_progress" then
ecs.container.set_state("ancient_tomb", {
{ itemId = "potion_health", stackSize = 2 },
{ itemId = "gold_coin", stackSize = 50 },
{ itemId = "amulet_legendary", stackSize = 1 }
})
elseif quest_stage == "completed" then
ecs.container.clear_state("ancient_tomb")
end
print("Quest container 'ancient_tomb' set to stage: " .. quest_stage)
end
setup_quest_container("not_started")
setup_quest_container("in_progress")
setup_quest_container("completed")
-- =============================================================================
-- API Reference
-- =============================================================================
--
-- ecs.container.set_state(container_id, slots_table) -> nil
-- Sets the persistent state of a container.
-- Each slot: { itemId = "...", stackSize = N }
--
-- ecs.container.get_state(container_id) -> table of { itemId, stackSize }
-- Returns the current state of a container.
--
-- ecs.container.clear_state(container_id) -> nil
-- Clears all items from a container's state.
--
-- Container state is separate from entity inventories (InventoryComponent).
-- Use containers for persistent world objects like chests, shops, and
-- quest lootables that need to remember their state across sessions.
-- =============================================================================
print("Container examples completed successfully!")
@@ -0,0 +1,211 @@
-- =============================================================================
-- Dialogue Lua API Examples
-- =============================================================================
-- This file demonstrates how to use the ecs.dialogue API for managing
-- in-game dialogue boxes with text, choices, speaker names, and settings.
--
-- The dialogue system provides:
-- - Show/hide dialogue boxes with text and optional choices
-- - Speaker name display
-- - Choice selection and progression
-- - Settings management (font, opacity, positioning)
-- - Settings persistence (save/load to JSON)
-- =============================================================================
-- =============================================================================
-- Basic Dialogue Display
-- =============================================================================
-- Show a simple dialogue with text only
ecs.dialogue.show("Hello, traveler! Welcome to our village.")
print("Showed basic dialogue")
-- Show dialogue with speaker name
ecs.dialogue.show("I have a quest for you.", {}, "Elder Marcus")
print("Showed dialogue with speaker")
-- =============================================================================
-- Dialogue with Choices
-- =============================================================================
-- Show dialogue with multiple choices
ecs.dialogue.show("What would you like to do?", {
"Ask about the quest",
"Browse his wares",
"Say goodbye"
}, "Shopkeeper")
print("Showed dialogue with choices")
-- Select a choice (simulates player clicking option 1)
ecs.dialogue.select_choice(1)
print("Selected choice 1")
-- =============================================================================
-- Hiding Dialogue
-- =============================================================================
-- Hide the current dialogue
ecs.dialogue.hide()
print("Dialogue hidden")
-- =============================================================================
-- Checking Dialogue State
-- =============================================================================
-- Check if dialogue is currently active
local active = ecs.dialogue.is_active()
print("Dialogue active: " .. tostring(active))
-- =============================================================================
-- Progressing Through Dialogue
-- =============================================================================
-- Show a multi-line narration and progress through it
ecs.dialogue.show("The sun sets over the horizon...")
ecs.dialogue.progress()
ecs.dialogue.show("A cool breeze sweeps through the valley...")
ecs.dialogue.progress()
ecs.dialogue.show("You hear footsteps in the distance.")
ecs.dialogue.progress()
print("Progressed through narration")
-- =============================================================================
-- Dialogue Settings Management
-- =============================================================================
-- Get current settings
local settings = ecs.dialogue.get_settings()
print("Current settings:")
print(" Font: " .. settings.font_name)
print(" Font size: " .. settings.font_size)
print(" Speaker font size: " .. settings.speaker_font_size)
print(" Background opacity: " .. settings.background_opacity)
print(" Box height fraction: " .. settings.box_height_fraction)
print(" Box position fraction: " .. settings.box_position_fraction)
-- Modify settings
ecs.dialogue.set_settings({
font_name = "Jupiteroid-Regular.ttf",
font_size = 20.0,
speaker_font_size = 18.0,
background_opacity = 0.85,
box_height_fraction = 0.25,
box_position_fraction = 0.75
})
print("Updated dialogue settings")
-- Verify the changes
local updated = ecs.dialogue.get_settings()
print("Updated font size: " .. updated.font_size)
-- =============================================================================
-- Saving and Loading Settings
-- =============================================================================
-- Save settings to default path (dialogue.json)
local saved = ecs.dialogue.save_settings()
print("Settings saved: " .. tostring(saved))
-- Save settings to a custom path
local saved_custom = ecs.dialogue.save_settings("my_dialogue_config.json")
print("Settings saved to custom path: " .. tostring(saved_custom))
-- Load settings from default path
local loaded = ecs.dialogue.load_settings()
print("Settings loaded: " .. tostring(loaded))
-- Load settings from custom path
local loaded_custom = ecs.dialogue.load_settings("my_dialogue_config.json")
print("Settings loaded from custom path: " .. tostring(loaded_custom))
-- =============================================================================
-- Practical: Dialogue Sequence with Choices
-- =============================================================================
-- A simple dialogue tree simulation
function run_dialogue_tree()
-- Node 1: Greeting
ecs.dialogue.show("Greetings, adventurer! How can I help you?", {
"Tell me about the local area",
"I need supplies",
"I'm just passing through"
}, "Innkeeper")
-- Simulate player choosing option 1
ecs.dialogue.select_choice(1)
-- Node 2: Response to choice 1
ecs.dialogue.show("Ah, you must be new here! This town has a rich history. "
.. "To the north lies the ancient forest, and to the east, "
.. "the old ruins.", {}, "Innkeeper")
ecs.dialogue.progress()
-- Node 3: Offer quest
ecs.dialogue.show("If you're looking for adventure, I heard the ruins "
.. "hold a legendary treasure. But beware of the traps!", {
"I'll check it out!",
"Sounds too dangerous"
}, "Innkeeper")
-- Simulate player choosing option 1
ecs.dialogue.select_choice(1)
-- Node 4: Final response
ecs.dialogue.show("Excellent! Good luck on your journey, adventurer!", {}, "Innkeeper")
ecs.dialogue.progress()
-- End dialogue
ecs.dialogue.hide()
print("Dialogue tree completed")
end
run_dialogue_tree()
-- =============================================================================
-- API Reference
-- =============================================================================
--
-- ecs.dialogue.show(text, choices?, speaker?)
-- text - string, the dialogue text to display
-- choices - optional table of strings, player response options
-- speaker - optional string, name of the speaking character
--
-- ecs.dialogue.hide()
-- Hides the current dialogue box.
--
-- ecs.dialogue.is_active() -> bool
-- Returns true if a dialogue is currently being displayed.
--
-- ecs.dialogue.select_choice(index)
-- Simulates selecting a choice by its 1-based index.
--
-- ecs.dialogue.progress()
-- Advances to the next line of dialogue (click-to-dismiss).
--
-- ecs.dialogue.get_settings() -> table
-- Returns a table with fields:
-- font_name, font_size, speaker_font_size,
-- background_opacity, box_height_fraction, box_position_fraction
--
-- ecs.dialogue.set_settings(settings_table)
-- Updates dialogue display settings. Partial tables are accepted.
--
-- ecs.dialogue.save_settings(path?) -> bool
-- Saves current settings to JSON. Default path: "dialogue.json"
--
-- ecs.dialogue.load_settings(path?) -> bool
-- Loads settings from JSON. Default path: "dialogue.json"
-- =============================================================================
print("Dialogue examples completed successfully!")
@@ -0,0 +1,198 @@
-- =============================================================================
-- Inventory Lua API Examples
-- =============================================================================
-- This file demonstrates how to manage entity inventories using the
-- ecs.inventory API. Inventories are stored as InventoryComponent on
-- ECS entities.
--
-- Prerequisites:
-- - Items must be registered via ecs.items.register() first
-- - Entities must have an InventoryComponent (added via ecs.add_component)
-- =============================================================================
-- =============================================================================
-- Setup: Create an entity with an inventory
-- =============================================================================
-- Create a player entity
local player = ecs.create_entity()
ecs.set_entity_name(player, "player")
-- Add an InventoryComponent with 20 slots and 50.0 max weight
ecs.set_component(player, "Inventory", {
maxSlots = 20,
maxWeight = 50.0,
isContainer = false,
containerId = ""
})
print("Created player entity with inventory (ID: " .. player .. ")")
-- =============================================================================
-- Adding Items to Inventory
-- =============================================================================
-- Add 5 health potions
local added = ecs.inventory.add(player, "potion_health", 5)
print("Added 5 health potions: " .. tostring(added))
-- Add 3 arrows
ecs.inventory.add(player, "arrow", 3)
print("Added 3 arrows")
-- Add 1 iron sword
ecs.inventory.add(player, "sword_iron", 1)
print("Added 1 iron sword")
-- Add 50 gold coins
ecs.inventory.add(player, "gold_coin", 50)
print("Added 50 gold coins")
-- =============================================================================
-- Checking Inventory Contents
-- =============================================================================
-- Check if the player has a specific item
if ecs.inventory.has(player, "potion_health") then
print("Player has health potions")
end
-- Count how many of a specific item
local potion_count = ecs.inventory.count(player, "potion_health")
print("Health potion count: " .. potion_count)
local gold_count = ecs.inventory.count(player, "gold_coin")
print("Gold coin count: " .. gold_count)
-- =============================================================================
-- Listing Inventory Slots
-- =============================================================================
-- Get all non-empty slots
local slots = ecs.inventory.get_slots(player)
print("Inventory slots (" .. #slots .. " non-empty):")
for i, slot in ipairs(slots) do
print(" [" .. i .. "] " .. slot.itemId .. " x" .. slot.stackSize)
end
-- =============================================================================
-- Removing Items from Inventory
-- =============================================================================
-- Remove 2 health potions
local removed = ecs.inventory.remove(player, "potion_health", 2)
print("Removed " .. removed .. " health potions")
-- Check remaining count
local remaining = ecs.inventory.count(player, "potion_health")
print("Remaining health potions: " .. remaining)
-- =============================================================================
-- Setting Inventory Slots Directly
-- =============================================================================
-- Replace the entire inventory contents
ecs.inventory.set_slots(player, {
{ itemId = "potion_health", stackSize = 3 },
{ itemId = "sword_iron", stackSize = 1 },
{ itemId = "gold_coin", stackSize = 100 }
})
print("Inventory slots replaced via set_slots")
-- Verify the new contents
local new_slots = ecs.inventory.get_slots(player)
print("New inventory slots (" .. #new_slots .. "):")
for i, slot in ipairs(new_slots) do
print(" [" .. i .. "] " .. slot.itemId .. " x" .. slot.stackSize)
end
-- =============================================================================
-- Practical: Gold Management
-- =============================================================================
function add_gold(entity, amount)
local current = ecs.inventory.count(entity, "gold_coin")
ecs.inventory.add(entity, "gold_coin", amount)
print("Gold: " .. current .. " -> " .. (current + amount))
end
function remove_gold(entity, amount)
local current = ecs.inventory.count(entity, "gold_coin")
if current >= amount then
ecs.inventory.remove(entity, "gold_coin", amount)
print("Gold: " .. current .. " -> " .. (current - amount))
return true
else
print("Not enough gold! Have " .. current .. ", need " .. amount)
return false
end
end
add_gold(player, 50)
remove_gold(player, 30)
remove_gold(player, 200) -- Should fail
-- =============================================================================
-- Practical: Item Transfer Between Entities
-- =============================================================================
-- Create a chest entity
local chest = ecs.create_entity()
ecs.set_entity_name(chest, "chest_wooden")
ecs.set_component(chest, "Inventory", {
maxSlots = 10,
maxWeight = 100.0,
isContainer = true,
containerId = "chest_wooden_001"
})
-- Add items to chest
ecs.inventory.add(chest, "bow_wood", 1)
ecs.inventory.add(chest, "arrow", 20)
ecs.inventory.add(chest, "potion_health", 2)
-- Transfer function: move items from one entity to another
function transfer_item(from_entity, to_entity, item_id, count)
local available = ecs.inventory.count(from_entity, item_id)
if available < count then
print("Not enough " .. item_id .. " to transfer (have " .. available .. ")")
return false
end
ecs.inventory.remove(from_entity, item_id, count)
ecs.inventory.add(to_entity, item_id, count)
print("Transferred " .. count .. " " .. item_id .. " from " ..
ecs.get_entity_name(from_entity) .. " to " ..
ecs.get_entity_name(to_entity))
return true
end
-- Transfer arrows from chest to player
transfer_item(chest, player, "arrow", 10)
-- =============================================================================
-- API Reference
-- =============================================================================
--
-- ecs.inventory.add(entity_id, item_id, count) -> bool
-- Adds items to the entity's inventory. Returns true if successful.
--
-- ecs.inventory.remove(entity_id, item_id, count) -> int
-- Removes items and returns the number actually removed.
--
-- ecs.inventory.has(entity_id, item_id) -> bool
-- Returns true if the entity has at least one of the item.
--
-- ecs.inventory.count(entity_id, item_id) -> int
-- Returns the total count of the item across all slots.
--
-- ecs.inventory.get_slots(entity_id) -> table of { itemId, stackSize }
-- Returns all non-empty inventory slots.
--
-- ecs.inventory.set_slots(entity_id, slots_table) -> nil
-- Replaces the entire inventory with the given slots.
-- Each slot: { itemId = "...", stackSize = N }
-- =============================================================================
print("Inventory examples completed successfully!")
@@ -0,0 +1,144 @@
-- =============================================================================
-- Item Registry Lua API Examples
-- =============================================================================
-- This file demonstrates how to register item definitions using the
-- ecs.items API. Run this from data.lua or any other Lua entry point
-- to populate the global item registry.
--
-- The ItemRegistry is a global singleton. Items defined here are
-- immediately available for use in inventories, containers, and quests.
-- =============================================================================
-- =============================================================================
-- Registering Items
-- =============================================================================
-- Consumables
ecs.items.register("potion_health", {
itemName = "Health Potion",
itemType = "consumable",
maxStackSize = 10,
weight = 0.5,
value = 25,
useActionName = "drink_potion",
unique = false
})
ecs.items.register("potion_stamina", {
itemName = "Stamina Potion",
itemType = "consumable",
maxStackSize = 10,
weight = 0.5,
value = 20,
useActionName = "drink_potion",
unique = false
})
-- Weapons
ecs.items.register("sword_iron", {
itemName = "Iron Sword",
itemType = "weapon",
maxStackSize = 1,
weight = 3.5,
value = 100,
useActionName = "equip_weapon",
unique = false
})
ecs.items.register("bow_wood", {
itemName = "Wooden Bow",
itemType = "weapon",
maxStackSize = 1,
weight = 2.0,
value = 75,
useActionName = "equip_weapon",
unique = false
})
-- Ammo
ecs.items.register("arrow", {
itemName = "Arrow",
itemType = "ammo",
maxStackSize = 99,
weight = 0.1,
value = 2,
useActionName = "",
unique = false
})
-- Unique quest item
ecs.items.register("amulet_legendary", {
itemName = "Amulet of the Ancients",
itemType = "quest",
maxStackSize = 1,
weight = 0.2,
value = 5000,
useActionName = "inspect_amulet",
unique = true
})
-- Currency
ecs.items.register("gold_coin", {
itemName = "Gold Coin",
itemType = "currency",
maxStackSize = 999,
weight = 0.01,
value = 1,
useActionName = "",
unique = false
})
-- =============================================================================
-- Querying the Item Registry
-- =============================================================================
-- Find an item by ID:
local potion = ecs.items.find("potion_health")
if potion then
print("Found item: " .. potion.itemName .. " (" .. potion.itemType .. ")")
print(" Max stack: " .. potion.maxStackSize)
print(" Weight: " .. potion.weight)
print(" Value: " .. potion.value)
print(" Unique: " .. tostring(potion.unique))
end
-- Check if an item is unique:
if ecs.items.is_unique("amulet_legendary") then
print("Amulet of the Ancients is a unique item")
end
-- List all registered items:
print("Registered items:")
local all_items = ecs.items.list()
for _, id in ipairs(all_items) do
local def = ecs.items.find(id)
print(" " .. id .. " - " .. def.itemName .. " (" .. def.itemType .. ")")
end
-- =============================================================================
-- Item Definition Fields Reference
-- =============================================================================
--
-- ecs.items.register(itemId, definition)
-- itemId - string, unique identifier for the item
-- definition - table with fields:
-- itemName - string, display name
-- itemType - string, category ("consumable", "weapon", "ammo",
-- "quest", "currency", "material", "armor", etc.)
-- maxStackSize - integer, maximum items per inventory slot
-- weight - number, weight per item
-- value - integer, base value in gold
-- useActionName - string, GOAP action name when used (empty = no action)
-- unique - boolean, true if only one instance allowed per character
--
-- ecs.items.find(itemId) -> table or nil
-- Returns the item definition table with all fields above.
--
-- ecs.items.list() -> table of item ID strings
-- Returns all registered item IDs.
--
-- ecs.items.is_unique(itemId) -> boolean
-- Returns true if the item is marked as unique.
-- =============================================================================
print("Item registry examples completed successfully!")
@@ -0,0 +1,248 @@
-- =============================================================================
-- Quest Reward Lua API Examples
-- =============================================================================
-- This file demonstrates how to use the inventory and container APIs
-- together to implement quest reward systems.
--
-- This combines:
-- - ecs.inventory.* for player inventory management
-- - ecs.container.* for quest container state
-- - ecs.items.* for item registry queries
-- - ecs.send_event / ecs.subscribe_event for event-driven quest flow
-- =============================================================================
-- =============================================================================
-- Setup: Create player and quest-related entities
-- =============================================================================
-- Create the player entity with an inventory
local player = ecs.create_entity()
ecs.set_entity_name(player, "player")
ecs.set_component(player, "Inventory", {
maxSlots = 30,
maxWeight = 100.0,
isContainer = false,
containerId = ""
})
print("Created player entity (ID: " .. player .. ")")
-- Create a quest giver NPC
local quest_giver = ecs.create_entity()
ecs.set_entity_name(quest_giver, "quest_giver_elder")
ecs.set_component(quest_giver, "Inventory", {
maxSlots = 10,
maxWeight = 50.0,
isContainer = false,
containerId = ""
})
print("Created quest giver entity (ID: " .. quest_giver .. ")")
-- =============================================================================
-- Quest Reward Distribution
-- =============================================================================
-- Give the player starting equipment
ecs.inventory.add(player, "sword_iron", 1)
ecs.inventory.add(player, "potion_health", 3)
ecs.inventory.add(player, "gold_coin", 25)
print("Player starting equipment added")
-- Give the quest giver some reward items
ecs.inventory.add(quest_giver, "gold_coin", 200)
ecs.inventory.add(quest_giver, "potion_health", 2)
ecs.inventory.add(quest_giver, "amulet_legendary", 1)
print("Quest giver reward items added")
-- =============================================================================
-- Quest Completion: Reward Player
-- =============================================================================
-- Complete a quest and give the player rewards
function complete_quest(quest_name, reward_items, xp_reward)
print("=== Quest Completed: " .. quest_name .. " ===")
print("XP Reward: " .. xp_reward)
-- Give each reward item to the player
for _, reward in ipairs(reward_items) do
local item_def = ecs.items.find(reward.itemId)
if item_def then
ecs.inventory.add(player, reward.itemId, reward.count)
print(" Received: " .. reward.count .. "x " .. item_def.itemName)
else
print(" WARNING: Unknown item '" .. reward.itemId .. "'")
end
end
-- Send a quest completion event
ecs.send_event("quest_completed", {
quest_name = quest_name,
player_id = player,
xp_rewarded = xp_reward
})
print("Quest completion event sent")
end
-- Complete "The Lost Artifact" quest
complete_quest("The Lost Artifact", {
{ itemId = "gold_coin", count = 100 },
{ itemId = "potion_health", count = 2 }
}, 500)
-- =============================================================================
-- Quest Item Requirement Check
-- =============================================================================
-- Check if the player has the required items for a quest
function has_quest_items(quest_requirements)
for _, req in ipairs(quest_requirements) do
local count = ecs.inventory.count(player, req.itemId)
if count < req.count then
local def = ecs.items.find(req.itemId)
local name = def and def.itemName or req.itemId
print(" Missing: " .. (req.count - count) .. " more " .. name)
return false
end
end
return true
end
-- Remove quest items from the player's inventory (turn in)
function remove_quest_items(quest_requirements)
for _, req in ipairs(quest_requirements) do
ecs.inventory.remove(player, req.itemId, req.count)
local def = ecs.items.find(req.itemId)
local name = def and def.itemName or req.itemId
print(" Removed: " .. req.count .. "x " .. name)
end
end
-- Define a quest that requires items
local bandit_quest = {
name = "Bandit Menace",
requirements = {
{ itemId = "sword_iron", count = 1 },
{ itemId = "potion_health", count = 2 }
},
rewards = {
{ itemId = "gold_coin", count = 150 },
{ itemId = "bow_wood", count = 1 }
},
xp = 750
}
-- Check if player can start the quest
print("Checking quest requirements for '" .. bandit_quest.name .. "':")
if has_quest_items(bandit_quest.requirements) then
print("Player has all required items!")
remove_quest_items(bandit_quest.requirements)
complete_quest(bandit_quest.name, bandit_quest.rewards, bandit_quest.xp)
else
print("Player does not meet quest requirements")
end
-- =============================================================================
-- Quest Container Loot
-- =============================================================================
-- Set up a quest-related container (e.g., a treasure chest at the quest location)
ecs.container.set_state("bandit_hideout_chest", {
{ itemId = "gold_coin", stackSize = 200 },
{ itemId = "potion_health", stackSize = 2 },
{ itemId = "potion_stamina", stackSize = 1 }
})
print("Bandit hideout chest populated with loot")
-- Player loots the chest
local chest_loot = ecs.container.get_state("bandit_hideout_chest")
print("Looting bandit hideout chest:")
for _, slot in ipairs(chest_loot) do
ecs.inventory.add(player, slot.itemId, slot.stackSize)
local def = ecs.items.find(slot.itemId)
local name = def and def.itemName or slot.itemId
print(" Acquired: " .. slot.stackSize .. "x " .. name)
end
-- Clear the chest after looting
ecs.container.clear_state("bandit_hideout_chest")
print("Chest cleared after looting")
-- =============================================================================
-- Quest Progression with Event Subscriptions
-- =============================================================================
-- Track quest state
local quest_state = {
active_quests = {},
completed_quests = {}
}
-- Subscribe to quest completion events
local quest_sub = ecs.subscribe_event("quest_completed", function(event, params)
local quest_name = params.quest_name or "unknown"
local xp = params.xp_rewarded or 0
-- Track completed quests
table.insert(quest_state.completed_quests, quest_name)
print("[Quest Log] Completed: " .. quest_name .. " (+" .. xp .. " XP)")
print("[Quest Log] Total completed: " .. #quest_state.completed_quests)
end)
print("Subscribed to quest_completed events (ID: " .. quest_sub .. ")")
-- =============================================================================
-- Practical: Full Quest Flow
-- =============================================================================
function run_quest_flow(quest_name, requirements, rewards, xp)
print("\n=== Starting Quest: " .. quest_name .. " ===")
-- 1. Check requirements
if not has_quest_items(requirements) then
print("Cannot start quest - missing items")
return false
end
-- 2. Remove quest items
print("Turning in quest items:")
remove_quest_items(requirements)
-- 3. Complete quest and give rewards
complete_quest(quest_name, rewards, xp)
return true
end
-- Run a second quest
run_quest_flow(
"The Blacksmith's Request",
{
{ itemId = "gold_coin", count = 50 }
},
{
{ itemId = "sword_iron", count = 1 },
{ itemId = "gold_coin", count = 75 }
},
300
)
-- =============================================================================
-- Summary
-- =============================================================================
-- Quest reward patterns demonstrated:
--
-- 1. Direct reward: ecs.inventory.add() to give items to the player
-- 2. Requirement check: ecs.inventory.count() to verify quest items
-- 3. Item removal: ecs.inventory.remove() to consume quest items
-- 4. Container loot: ecs.container.get_state() + ecs.inventory.add()
-- 5. Event-driven: ecs.send_event() + ecs.subscribe_event() for quest flow
-- 6. Item lookup: ecs.items.find() for display names and metadata
-- =============================================================================
print("Quest reward examples completed successfully!")
@@ -833,11 +833,26 @@ static void registerAllComponents()
lua_setfield(L, -2, "itemId");
lua_pushinteger(L, c.stackSize);
lua_setfield(L, -2, "stackSize");
lua_pushstring(L, c.action.c_str());
lua_setfield(L, -2, "action");
lua_pushstring(L, c.instanceId.c_str());
lua_setfield(L, -2, "instanceId");
lua_pushboolean(L, c.disabled ? 1 : 0);
lua_setfield(L, -2, "disabled");
, 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, "action"), lua_isstring(L, -1))
c.action = lua_tostring(L, -1);
lua_pop(L, 1);
if (lua_getfield(L, idx, "instanceId"), lua_isstring(L, -1))
c.instanceId = lua_tostring(L, -1);
lua_pop(L, 1);
if (lua_getfield(L, idx, "disabled"), lua_isboolean(L, -1))
c.disabled = lua_toboolean(L, -1) != 0;
lua_pop(L, 1););
// --- Inventory ---
@@ -3,6 +3,7 @@
#include "BehaviorTreeSystem.hpp"
#include "ItemSystem.hpp"
#include "ItemRegistry.hpp"
#include "ItemStateRegistry.hpp"
#include "../components/Actuator.hpp"
#include "../components/Item.hpp"
#include "../components/Inventory.hpp"
@@ -111,6 +112,28 @@ void ActuatorSystem::executeAction(flecs::entity character,
"[ActuatorSystem] Executing action: " + actionName);
}
void ActuatorSystem::executeItemAction(flecs::entity character,
flecs::entity itemEntity,
const Ogre::String &actionName)
{
if (!character.is_alive() || !itemEntity.is_alive())
return;
if (!itemEntity.has<ItemComponent>())
return;
m_executingActuatorId = itemEntity.id();
m_executingCharacterId = character.id();
m_executingActionName = actionName;
m_actionFirstFrame = true;
// Lock player input while action executes
setPlayerInputLocked(true);
Ogre::LogManager::getSingleton().logMessage(
"[ActuatorSystem] Executing item action: " + actionName);
}
bool ActuatorSystem::isActionComplete(flecs::entity character, float deltaTime)
{
if (!m_btSystem || !character.is_alive())
@@ -468,10 +491,38 @@ void ActuatorSystem::update(float deltaTime)
}
} else if (targetEntity.has<ItemComponent>() &&
input.ePressed) {
// Pick up item
if (m_itemSystem) {
m_itemSystem->pickupItem(playerCharacter,
targetEntity);
auto &item = targetEntity.get_mut<ItemComponent>();
if (!item.action.empty()) {
// Execute the item's action via behavior tree
executeItemAction(playerCharacter,
targetEntity,
item.action);
} else {
// Pick up item
if (m_itemSystem) {
m_itemSystem->pickupItem(
playerCharacter,
targetEntity);
}
// Disable the item and persist state
item.disabled = true;
if (!item.instanceId.empty()) {
ItemStateRegistry::getInstance()
.setDisabled(item.instanceId,
true);
}
// Hide the entity in the scene
if (targetEntity.has<
TransformComponent>()) {
auto &trans = targetEntity
.get_mut<
TransformComponent>();
if (trans.node) {
trans.node->setVisible(
false);
}
}
}
m_eHoldTime = 0.0f;
}
@@ -62,6 +62,9 @@ private:
void executeAction(flecs::entity character,
flecs::entity actuatorEntity,
const Ogre::String &actionName);
void executeItemAction(flecs::entity character,
flecs::entity itemEntity,
const Ogre::String &actionName);
bool isActionComplete(flecs::entity character, float deltaTime);
void drawActionMenu(flecs::entity actuatorEntity);
void setPlayerInputLocked(bool locked);
@@ -3,6 +3,8 @@
#include "CharacterSystem.hpp"
#include "SmartObjectSystem.hpp"
#include "ItemSystem.hpp"
#include "ItemRegistry.hpp"
#include "ItemStateRegistry.hpp"
#include "EventBus.hpp"
#include "../components/BehaviorTree.hpp"
#include "../components/ActionDatabase.hpp"
@@ -946,6 +948,51 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
return Status::success;
}
/* --- Disable item entity (for pickup / consumption) --- */
if (node.type == "disableItem") {
if (isNewlyActive(state, &node)) {
// node.name can specify an instanceId override,
// otherwise we look for an ItemComponent on this entity
flecs::entity targetEntity = e;
if (!node.name.empty()) {
m_world.query<ItemComponent>().each(
[&](flecs::entity itemEntity,
ItemComponent &itemComp) {
if (itemComp.instanceId ==
node.name.c_str() &&
!targetEntity.is_alive()) {
targetEntity = itemEntity;
}
});
}
if (!targetEntity.is_alive() ||
!targetEntity.has<ItemComponent>()) {
std::cout << "[BT] disableItem: no item entity"
<< std::endl;
return Status::failure;
}
auto &item = targetEntity.get_mut<ItemComponent>();
item.disabled = true;
if (!item.instanceId.empty()) {
ItemStateRegistry::getInstance().setDisabled(
item.instanceId, true);
}
if (targetEntity.has<TransformComponent>()) {
auto &trans = targetEntity.get_mut<
TransformComponent>();
if (trans.node)
trans.node->setVisible(false);
}
std::cout << "[BT] disableItem: disabled item "
<< targetEntity.id() << std::endl;
return Status::success;
}
return Status::success;
}
/* --- Lua behavior tree node --- */
if (node.type == "luaTask") {
/* Call the Lua function via the forward-declared API.
@@ -0,0 +1,109 @@
#include "ItemStateRegistry.hpp"
#include <OgreLogManager.h>
#include <fstream>
ItemStateRegistry &ItemStateRegistry::getInstance()
{
static ItemStateRegistry instance;
return instance;
}
ItemStateRegistry::ItemStateRegistry()
{
m_autoSavePath = "item_state.json";
}
void ItemStateRegistry::setDisabled(const std::string &instanceId,
bool disabled)
{
if (instanceId.empty())
return;
ItemState &state = m_states[instanceId];
state.instanceId = instanceId;
state.disabled = disabled;
autoSave();
}
bool ItemStateRegistry::isDisabled(const std::string &instanceId) const
{
if (instanceId.empty())
return false;
auto it = m_states.find(instanceId);
if (it != m_states.end())
return it->second.disabled;
return false;
}
void ItemStateRegistry::clearState(const std::string &instanceId)
{
m_states.erase(instanceId);
autoSave();
}
nlohmann::json ItemStateRegistry::serialize() const
{
nlohmann::json j;
j["version"] = "1.0";
for (const auto &pair : m_states) {
const ItemState &state = pair.second;
nlohmann::json stateJson;
stateJson["instanceId"] = state.instanceId;
stateJson["disabled"] = state.disabled;
j["states"].push_back(stateJson);
}
return j;
}
void ItemStateRegistry::deserialize(const nlohmann::json &j)
{
m_states.clear();
if (!j.contains("states"))
return;
for (const auto &stateJson : j["states"]) {
ItemState state;
state.instanceId = stateJson.value("instanceId", "");
state.disabled = stateJson.value("disabled", false);
if (!state.instanceId.empty())
m_states[state.instanceId] = state;
}
}
bool ItemStateRegistry::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 ItemStateRegistry::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 ItemStateRegistry::autoSave()
{
if (!m_autoSavePath.empty())
saveToFile(m_autoSavePath);
}
@@ -0,0 +1,55 @@
#ifndef EDITSCENE_ITEMSTATEREGISTRY_HPP
#define EDITSCENE_ITEMSTATEREGISTRY_HPP
#pragma once
#include <nlohmann/json.hpp>
#include <string>
#include <unordered_map>
/**
* Global item state registry.
*
* Tracks per-instance item state keyed by instanceId.
* Used to persist whether world items have been picked up / disabled
* across scene reloads and game sessions.
*
* Saves to item_state.json.
*/
class ItemStateRegistry {
public:
static ItemStateRegistry &getInstance();
struct ItemState {
std::string instanceId;
bool disabled = false;
};
void setDisabled(const std::string &instanceId, bool disabled);
bool isDisabled(const std::string &instanceId) const;
void clearState(const std::string &instanceId);
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:
ItemStateRegistry();
~ItemStateRegistry() = default;
ItemStateRegistry(const ItemStateRegistry &) = delete;
ItemStateRegistry &operator=(const ItemStateRegistry &) = delete;
std::unordered_map<std::string, ItemState> m_states;
mutable std::string m_lastError;
std::string m_autoSavePath;
};
#endif // EDITSCENE_ITEMSTATEREGISTRY_HPP
@@ -1,6 +1,7 @@
#include "SceneSerializer.hpp"
#include "ItemRegistry.hpp"
#include "ContainerStateRegistry.hpp"
#include "ItemStateRegistry.hpp"
#include "../components/Transform.hpp"
#include "../components/Renderable.hpp"
#include "../components/EntityName.hpp"
@@ -3831,6 +3832,12 @@ nlohmann::json SceneSerializer::serializeItem(flecs::entity entity)
nlohmann::json json;
json["itemId"] = item.itemId;
json["stackSize"] = item.stackSize;
if (!item.action.empty())
json["action"] = item.action;
if (!item.instanceId.empty())
json["instanceId"] = item.instanceId;
if (item.disabled)
json["disabled"] = true;
return json;
}
@@ -0,0 +1,656 @@
/**
* @file character_class_lua_test.cpp
* @brief Standalone test for the Lua Character Class API.
*
* Tests the ecs.character_class.* functions exposed via the ecs.* Lua API.
* Tests database queries (get_class_names, get_stat_names, get_skill_names,
* get_need_names, get_class, get_stat_kind) and per-entity runtime API
* (get_level, get_xp, add_xp, get_stat, get_skill, get_need,
* get_available_points, set_need, get_pool_current, get_pool_max,
* set_pool_current).
*
* Build with:
* g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \
* character_class_lua_test.cpp \
* lua_test_stubs.cpp \
* ../../lua/lua-5.4.8/src/liblua.a \
* -o character_class_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 registerLuaCharacterClassApi(lua_State *L);
void registerLuaEntityApi(lua_State *L);
void registerLuaComponentApi(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: get_class_names returns a table
// ---------------------------------------------------------------------------
static int testGetClassNames(lua_State *L)
{
TEST("get_class_names returns a table");
bool ok = runLua(L,
"local names = ecs.character_class.get_class_names();"
"assert(type(names) == 'table', "
"'get_class_names should return a table')");
if (!ok)
FAIL("get_class_names assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 2: get_stat_names returns a table
// ---------------------------------------------------------------------------
static int testGetStatNames(lua_State *L)
{
TEST("get_stat_names returns a table");
bool ok = runLua(L,
"local names = ecs.character_class.get_stat_names();"
"assert(type(names) == 'table', "
"'get_stat_names should return a table')");
if (!ok)
FAIL("get_stat_names assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 3: get_skill_names returns a table
// ---------------------------------------------------------------------------
static int testGetSkillNames(lua_State *L)
{
TEST("get_skill_names returns a table");
bool ok = runLua(L,
"local names = ecs.character_class.get_skill_names();"
"assert(type(names) == 'table', "
"'get_skill_names should return a table')");
if (!ok)
FAIL("get_skill_names assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 4: get_need_names returns a table
// ---------------------------------------------------------------------------
static int testGetNeedNames(lua_State *L)
{
TEST("get_need_names returns a table");
bool ok = runLua(L,
"local names = ecs.character_class.get_need_names();"
"assert(type(names) == 'table', "
"'get_need_names should return a table')");
if (!ok)
FAIL("get_need_names assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 5: get_class returns nil for unknown class
// ---------------------------------------------------------------------------
static int testGetClassUnknown(lua_State *L)
{
TEST("get_class returns nil for unknown class");
bool ok = runLua(
L, "local cls = ecs.character_class.get_class('nonexistent');"
"assert(cls == nil, "
"'get_class should return nil for unknown class')");
if (!ok)
FAIL("get_class unknown assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 6: get_stat_kind returns 'unknown' for unknown stat
// ---------------------------------------------------------------------------
static int testGetStatKindUnknown(lua_State *L)
{
TEST("get_stat_kind returns 'unknown' for unknown stat");
bool ok = runLua(
L,
"local kind = ecs.character_class.get_stat_kind('nonexistent');"
"assert(kind == 'unknown', "
"'get_stat_kind should return unknown for unknown stat')");
if (!ok)
FAIL("get_stat_kind unknown assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 7: get_stat_kind returns 'unknown' for nil
// ---------------------------------------------------------------------------
static int testGetStatKindNil(lua_State *L)
{
TEST("get_stat_kind returns 'unknown' for nil");
bool ok = runLua(L,
"local kind = ecs.character_class.get_stat_kind(nil);"
"assert(kind == 'unknown', "
"'get_stat_kind should return unknown for nil')");
if (!ok)
FAIL("get_stat_kind nil assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 8: get_level returns 0 for non-existent entity
// ---------------------------------------------------------------------------
static int testGetLevelNoEntity(lua_State *L)
{
TEST("get_level returns 0 for non-existent entity");
bool ok = runLua(
L, "local level = ecs.character_class.get_level(99999);"
"assert(level == 0, "
"'get_level should return 0 for non-existent entity')");
if (!ok)
FAIL("get_level no entity assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 9: get_xp returns 0 for non-existent entity
// ---------------------------------------------------------------------------
static int testGetXPNoEntity(lua_State *L)
{
TEST("get_xp returns 0 for non-existent entity");
bool ok = runLua(L,
"local xp = ecs.character_class.get_xp(99999);"
"assert(xp == 0, "
"'get_xp should return 0 for non-existent entity')");
if (!ok)
FAIL("get_xp no entity assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 10: add_xp returns false for non-existent entity
// ---------------------------------------------------------------------------
static int testAddXPNoEntity(lua_State *L)
{
TEST("add_xp returns false for non-existent entity");
bool ok = runLua(
L, "local ok = ecs.character_class.add_xp(99999, 100);"
"assert(ok == false, "
"'add_xp should return false for non-existent entity')");
if (!ok)
FAIL("add_xp no entity assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 11: get_stat returns 0 for non-existent entity
// ---------------------------------------------------------------------------
static int testGetStatNoEntity(lua_State *L)
{
TEST("get_stat returns 0 for non-existent entity");
bool ok = runLua(
L,
"local val = ecs.character_class.get_stat(99999, 'strength');"
"assert(val == 0, "
"'get_stat should return 0 for non-existent entity')");
if (!ok)
FAIL("get_stat no entity assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 12: get_skill returns 0 for non-existent entity
// ---------------------------------------------------------------------------
static int testGetSkillNoEntity(lua_State *L)
{
TEST("get_skill returns 0 for non-existent entity");
bool ok = runLua(
L,
"local val = ecs.character_class.get_skill(99999, 'swordsmanship');"
"assert(val == 0, "
"'get_skill should return 0 for non-existent entity')");
if (!ok)
FAIL("get_skill no entity assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 13: get_need returns 0 for non-existent entity
// ---------------------------------------------------------------------------
static int testGetNeedNoEntity(lua_State *L)
{
TEST("get_need returns 0 for non-existent entity");
bool ok = runLua(
L, "local val = ecs.character_class.get_need(99999, 'hunger');"
"assert(val == 0, "
"'get_need should return 0 for non-existent entity')");
if (!ok)
FAIL("get_need no entity assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 14: get_available_points returns 0 for non-existent entity
// ---------------------------------------------------------------------------
static int testGetAvailablePointsNoEntity(lua_State *L)
{
TEST("get_available_points returns 0 for non-existent entity");
bool ok = runLua(
L,
"local pts = ecs.character_class.get_available_points(99999);"
"assert(pts == 0, "
"'get_available_points should return 0 for non-existent entity')");
if (!ok)
FAIL("get_available_points no entity assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 15: set_need does not crash for non-existent entity
// ---------------------------------------------------------------------------
static int testSetNeedNoEntity(lua_State *L)
{
TEST("set_need does not crash for non-existent entity");
bool ok =
runLua(L, "ecs.character_class.set_need(99999, 'hunger', 50);");
if (!ok)
FAIL("set_need no entity assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 16: get_pool_current returns 0 for non-existent entity
// ---------------------------------------------------------------------------
static int testGetPoolCurrentNoEntity(lua_State *L)
{
TEST("get_pool_current returns 0 for non-existent entity");
bool ok = runLua(
L,
"local val = ecs.character_class.get_pool_current(99999, 'health');"
"assert(val == 0, "
"'get_pool_current should return 0 for non-existent entity')");
if (!ok)
FAIL("get_pool_current no entity assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 17: get_pool_max returns 0 for non-existent entity
// ---------------------------------------------------------------------------
static int testGetPoolMaxNoEntity(lua_State *L)
{
TEST("get_pool_max returns 0 for non-existent entity");
bool ok = runLua(
L,
"local val = ecs.character_class.get_pool_max(99999, 'health');"
"assert(val == 0, "
"'get_pool_max should return 0 for non-existent entity')");
if (!ok)
FAIL("get_pool_max no entity assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 18: set_pool_current returns false for non-existent entity
// ---------------------------------------------------------------------------
static int testSetPoolCurrentNoEntity(lua_State *L)
{
TEST("set_pool_current returns false for non-existent entity");
bool ok = runLua(
L,
"local ok = ecs.character_class.set_pool_current(99999, 'health', 50);"
"assert(ok == false, "
"'set_pool_current should return false for non-existent entity')");
if (!ok)
FAIL("set_pool_current no entity assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 19: All functions exist and are callable
// ---------------------------------------------------------------------------
static int testAllFunctionsExist(lua_State *L)
{
TEST("all functions exist and are callable");
bool ok = runLua(
L,
"assert(type(ecs.character_class.get_class_names) == 'function', "
"'get_class_names should be a function');"
"assert(type(ecs.character_class.get_stat_names) == 'function', "
"'get_stat_names should be a function');"
"assert(type(ecs.character_class.get_skill_names) == 'function', "
"'get_skill_names should be a function');"
"assert(type(ecs.character_class.get_need_names) == 'function', "
"'get_need_names should be a function');"
"assert(type(ecs.character_class.get_class) == 'function', "
"'get_class should be a function');"
"assert(type(ecs.character_class.get_stat_kind) == 'function', "
"'get_stat_kind should be a function');"
"assert(type(ecs.character_class.get_level) == 'function', "
"'get_level should be a function');"
"assert(type(ecs.character_class.get_xp) == 'function', "
"'get_xp should be a function');"
"assert(type(ecs.character_class.add_xp) == 'function', "
"'add_xp should be a function');"
"assert(type(ecs.character_class.get_stat) == 'function', "
"'get_stat should be a function');"
"assert(type(ecs.character_class.get_skill) == 'function', "
"'get_skill should be a function');"
"assert(type(ecs.character_class.get_need) == 'function', "
"'get_need should be a function');"
"assert(type(ecs.character_class.get_available_points) == 'function', "
"'get_available_points should be a function');"
"assert(type(ecs.character_class.set_need) == 'function', "
"'set_need should be a function');"
"assert(type(ecs.character_class.get_pool_current) == 'function', "
"'get_pool_current should be a function');"
"assert(type(ecs.character_class.get_pool_max) == 'function', "
"'get_pool_max should be a function');"
"assert(type(ecs.character_class.set_pool_current) == 'function', "
"'set_pool_current should be a function')");
if (!ok)
FAIL("all functions exist assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 20: get_stat returns 0 for nil stat name
// ---------------------------------------------------------------------------
static int testGetStatNilName(lua_State *L)
{
TEST("get_stat returns 0 for nil stat name");
bool ok = runLua(L,
"local val = ecs.character_class.get_stat(99999, nil);"
"assert(val == 0, "
"'get_stat should return 0 for nil stat name')");
if (!ok)
FAIL("get_stat nil name assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 21: get_skill returns 0 for nil skill name
// ---------------------------------------------------------------------------
static int testGetSkillNilName(lua_State *L)
{
TEST("get_skill returns 0 for nil skill name");
bool ok = runLua(
L, "local val = ecs.character_class.get_skill(99999, nil);"
"assert(val == 0, "
"'get_skill should return 0 for nil skill name')");
if (!ok)
FAIL("get_skill nil name assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 22: get_need returns 0 for nil need name
// ---------------------------------------------------------------------------
static int testGetNeedNilName(lua_State *L)
{
TEST("get_need returns 0 for nil need name");
bool ok = runLua(L,
"local val = ecs.character_class.get_need(99999, nil);"
"assert(val == 0, "
"'get_need should return 0 for nil need name')");
if (!ok)
FAIL("get_need nil name assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 23: get_pool_current returns 0 for nil pool name
// ---------------------------------------------------------------------------
static int testGetPoolCurrentNilName(lua_State *L)
{
TEST("get_pool_current returns 0 for nil pool name");
bool ok = runLua(
L,
"local val = ecs.character_class.get_pool_current(99999, nil);"
"assert(val == 0, "
"'get_pool_current should return 0 for nil pool name')");
if (!ok)
FAIL("get_pool_current nil name assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 24: get_pool_max returns 0 for nil pool name
// ---------------------------------------------------------------------------
static int testGetPoolMaxNilName(lua_State *L)
{
TEST("get_pool_max returns 0 for nil pool name");
bool ok = runLua(
L, "local val = ecs.character_class.get_pool_max(99999, nil);"
"assert(val == 0, "
"'get_pool_max should return 0 for nil pool name')");
if (!ok)
FAIL("get_pool_max nil name assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 25: set_pool_current returns false for nil pool name
// ---------------------------------------------------------------------------
static int testSetPoolCurrentNilName(lua_State *L)
{
TEST("set_pool_current returns false for nil pool name");
bool ok = runLua(
L,
"local ok = ecs.character_class.set_pool_current(99999, nil, 50);"
"assert(ok == false, "
"'set_pool_current should return false for nil pool name')");
if (!ok)
FAIL("set_pool_current nil name assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
int main()
{
printf("Character Class 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 required APIs
editScene::registerLuaEntityApi(L);
editScene::registerLuaComponentApi(L);
editScene::registerLuaCharacterClassApi(L);
// Run tests
int failures = 0;
failures += testGetClassNames(L);
failures += testGetStatNames(L);
failures += testGetSkillNames(L);
failures += testGetNeedNames(L);
failures += testGetClassUnknown(L);
failures += testGetStatKindUnknown(L);
failures += testGetStatKindNil(L);
failures += testGetLevelNoEntity(L);
failures += testGetXPNoEntity(L);
failures += testAddXPNoEntity(L);
failures += testGetStatNoEntity(L);
failures += testGetSkillNoEntity(L);
failures += testGetNeedNoEntity(L);
failures += testGetAvailablePointsNoEntity(L);
failures += testSetNeedNoEntity(L);
failures += testGetPoolCurrentNoEntity(L);
failures += testGetPoolMaxNoEntity(L);
failures += testSetPoolCurrentNoEntity(L);
failures += testAllFunctionsExist(L);
failures += testGetStatNilName(L);
failures += testGetSkillNilName(L);
failures += testGetNeedNilName(L);
failures += testGetPoolCurrentNilName(L);
failures += testGetPoolMaxNilName(L);
failures += testSetPoolCurrentNilName(L);
// Cleanup
lua_close(L);
printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount,
failures);
return failures > 0 ? 1 : 0;
}
@@ -566,15 +566,21 @@ static int testItemComponent(lua_State *L)
bool ok = runLua(
L,
"local id = ecs.create_entity();"
"local id = ecs.create_entity(); "
"ecs.set_component(id, 'Item', {"
" itemId = 'potion_health',"
" stackSize = 5"
"});"
"local item = ecs.get_component(id, 'Item');"
"assert(item ~= nil, 'Item should exist');"
"assert(item.itemId == 'potion_health', 'wrong id');"
"assert(item.stackSize == 5, 'wrong stackSize')");
" stackSize = 5,"
" action = 'drink_potion',"
" instanceId = 'potion_001',"
" disabled = true"
"}); "
"local item = ecs.get_component(id, 'Item'); "
"assert(item ~= nil, 'Item should exist'); "
"assert(item.itemId == 'potion_health', 'wrong id'); "
"assert(item.stackSize == 5, 'wrong stackSize'); "
"assert(item.action == 'drink_potion', 'wrong action'); "
"assert(item.instanceId == 'potion_001', 'wrong instanceId'); "
"assert(item.disabled == true, 'wrong disabled')");
if (!ok)
FAIL("Item component assertion failed");
@@ -0,0 +1,432 @@
/**
* @file dialogue_lua_test.cpp
* @brief Standalone test for the Lua Dialogue API.
*
* Tests the ecs.dialogue.* functions exposed via the ecs.* Lua API.
* Tests show/hide, is_active, select_choice, progress, settings,
* and save/load settings.
*
* Build with:
* g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \
* dialogue_lua_test.cpp \
* lua_test_stubs.cpp \
* ../../lua/lua-5.4.8/src/liblua.a \
* -o dialogue_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 registerLuaDialogueApi(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: Show dialogue with text only
// ---------------------------------------------------------------------------
static int testShowTextOnly(lua_State *L)
{
TEST("show dialogue with text only");
bool ok = runLua(L, "ecs.dialogue.show('Hello world');"
"assert(ecs.dialogue.is_active() == false, "
"'stub returns false for is_active')");
if (!ok)
FAIL("show text only assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 2: Show dialogue with text and speaker
// ---------------------------------------------------------------------------
static int testShowWithSpeaker(lua_State *L)
{
TEST("show dialogue with text and speaker");
bool ok = runLua(L, "ecs.dialogue.show('Welcome!', {}, 'Narrator');"
"assert(ecs.dialogue.is_active() == false, "
"'stub returns false for is_active')");
if (!ok)
FAIL("show with speaker assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 3: Show dialogue with choices
// ---------------------------------------------------------------------------
static int testShowWithChoices(lua_State *L)
{
TEST("show dialogue with choices");
bool ok = runLua(L, "ecs.dialogue.show('Choose:', "
"{ 'Option A', 'Option B', 'Option C' }, 'Guide');"
"assert(ecs.dialogue.is_active() == false, "
"'stub returns false for is_active')");
if (!ok)
FAIL("show with choices assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 4: Hide dialogue
// ---------------------------------------------------------------------------
static int testHide(lua_State *L)
{
TEST("hide dialogue");
bool ok = runLua(L, "ecs.dialogue.show('Test');"
"ecs.dialogue.hide();"
"assert(ecs.dialogue.is_active() == false, "
"'should be inactive after hide')");
if (!ok)
FAIL("hide assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 5: Select choice
// ---------------------------------------------------------------------------
static int testSelectChoice(lua_State *L)
{
TEST("select choice");
bool ok = runLua(L, "ecs.dialogue.show('Pick one:', "
"{ 'A', 'B' }, 'Test');"
"ecs.dialogue.select_choice(1);"
"ecs.dialogue.select_choice(2);"
"ecs.dialogue.select_choice(0);");
if (!ok)
FAIL("select choice assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 6: Progress (click-to-dismiss)
// ---------------------------------------------------------------------------
static int testProgress(lua_State *L)
{
TEST("progress (click-to-dismiss)");
bool ok = runLua(L, "ecs.dialogue.show('Narration...');"
"ecs.dialogue.progress();");
if (!ok)
FAIL("progress assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 7: Get settings returns a table with expected fields
// ---------------------------------------------------------------------------
static int testGetSettings(lua_State *L)
{
TEST("get_settings returns table with expected fields");
bool ok = runLua(
L, "local s = ecs.dialogue.get_settings();"
"assert(type(s) == 'table', 'settings should be a table');"
"assert(type(s.font_name) == 'string', "
"'font_name should be string');"
"assert(type(s.font_size) == 'number', "
"'font_size should be number');"
"assert(type(s.speaker_font_size) == 'number', "
"'speaker_font_size should be number');"
"assert(type(s.background_opacity) == 'number', "
"'background_opacity should be number');"
"assert(type(s.box_height_fraction) == 'number', "
"'box_height_fraction should be number');"
"assert(type(s.box_position_fraction) == 'number', "
"'box_position_fraction should be number');");
if (!ok)
FAIL("get_settings assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 8: Set settings
// ---------------------------------------------------------------------------
static int testSetSettings(lua_State *L)
{
TEST("set_settings");
bool ok = runLua(L, "ecs.dialogue.set_settings({"
" font_name = 'Custom.ttf',"
" font_size = 18.0,"
" speaker_font_size = 16.0,"
" background_opacity = 0.9,"
" box_height_fraction = 0.3,"
" box_position_fraction = 0.7"
"});");
if (!ok)
FAIL("set_settings assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 9: Save settings returns boolean
// ---------------------------------------------------------------------------
static int testSaveSettings(lua_State *L)
{
TEST("save_settings returns boolean");
bool ok = runLua(
L, "local ok = ecs.dialogue.save_settings();"
"assert(type(ok) == 'boolean', "
"'save_settings should return boolean');"
"local ok2 = ecs.dialogue.save_settings('custom.json');"
"assert(type(ok2) == 'boolean', "
"'save_settings with path should return boolean')");
if (!ok)
FAIL("save_settings assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 10: Load settings returns boolean
// ---------------------------------------------------------------------------
static int testLoadSettings(lua_State *L)
{
TEST("load_settings returns boolean");
bool ok = runLua(
L, "local ok = ecs.dialogue.load_settings();"
"assert(type(ok) == 'boolean', "
"'load_settings should return boolean');"
"local ok2 = ecs.dialogue.load_settings('custom.json');"
"assert(type(ok2) == 'boolean', "
"'load_settings with path should return boolean')");
if (!ok)
FAIL("load_settings assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 11: Return types are correct for all functions
// ---------------------------------------------------------------------------
static int testReturnTypes(lua_State *L)
{
TEST("return types are correct");
bool ok = runLua(L, "assert(ecs.dialogue.show == nil or "
"type(ecs.dialogue.show) == 'function', "
"'show should be a function');"
"assert(ecs.dialogue.hide == nil or "
"type(ecs.dialogue.hide) == 'function', "
"'hide should be a function');"
"assert(ecs.dialogue.is_active == nil or "
"type(ecs.dialogue.is_active) == 'function', "
"'is_active should be a function');"
"assert(ecs.dialogue.select_choice == nil or "
"type(ecs.dialogue.select_choice) == 'function', "
"'select_choice should be a function');"
"assert(ecs.dialogue.progress == nil or "
"type(ecs.dialogue.progress) == 'function', "
"'progress should be a function');"
"assert(ecs.dialogue.get_settings == nil or "
"type(ecs.dialogue.get_settings) == 'function', "
"'get_settings should be a function');"
"assert(ecs.dialogue.set_settings == nil or "
"type(ecs.dialogue.set_settings) == 'function', "
"'set_settings should be a function');"
"assert(ecs.dialogue.save_settings == nil or "
"type(ecs.dialogue.save_settings) == 'function', "
"'save_settings should be a function');"
"assert(ecs.dialogue.load_settings == nil or "
"type(ecs.dialogue.load_settings) == 'function', "
"'load_settings should be a function')");
if (!ok)
FAIL("return types assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 12: Show with empty choices table
// ---------------------------------------------------------------------------
static int testShowEmptyChoices(lua_State *L)
{
TEST("show with empty choices table");
bool ok = runLua(L, "ecs.dialogue.show('No choices', {}, 'Speaker');"
"assert(ecs.dialogue.is_active() == false, "
"'stub returns false for is_active')");
if (!ok)
FAIL("show empty choices assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 13: Show with nil speaker
// ---------------------------------------------------------------------------
static int testShowNilSpeaker(lua_State *L)
{
TEST("show with nil speaker");
bool ok = runLua(L, "ecs.dialogue.show('No speaker');"
"assert(ecs.dialogue.is_active() == false, "
"'stub returns false for is_active')");
if (!ok)
FAIL("show nil speaker assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 14: Settings round-trip (get -> modify -> set -> get)
// ---------------------------------------------------------------------------
static int testSettingsRoundTrip(lua_State *L)
{
TEST("settings round-trip");
bool ok = runLua(L, "local s = ecs.dialogue.get_settings();"
"s.font_size = 30.0;"
"s.background_opacity = 0.5;"
"ecs.dialogue.set_settings(s);"
"local s2 = ecs.dialogue.get_settings();"
"assert(type(s2) == 'table', "
"'settings after set should be a table')");
if (!ok)
FAIL("settings round-trip assertion failed");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
int main()
{
printf("Dialogue 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 dialogue API
editScene::registerLuaDialogueApi(L);
// Run tests
int failures = 0;
failures += testShowTextOnly(L);
failures += testShowWithSpeaker(L);
failures += testShowWithChoices(L);
failures += testHide(L);
failures += testSelectChoice(L);
failures += testProgress(L);
failures += testGetSettings(L);
failures += testSetSettings(L);
failures += testSaveSettings(L);
failures += testLoadSettings(L);
failures += testReturnTypes(L);
failures += testShowEmptyChoices(L);
failures += testShowNilSpeaker(L);
failures += testSettingsRoundTrip(L);
// Cleanup
lua_close(L);
printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount,
failures);
return failures > 0 ? 1 : 0;
}
+196 -78
View File
@@ -149,15 +149,11 @@ void registerLuaDialogueApi(lua_State *L)
lua_newtable(L);
// show(text, choices, speaker)
lua_pushcfunction(L, [](lua_State *L) -> int {
return 0;
});
lua_pushcfunction(L, [](lua_State *L) -> int { return 0; });
lua_setfield(L, -2, "show");
// hide()
lua_pushcfunction(L, [](lua_State *L) -> int {
return 0;
});
lua_pushcfunction(L, [](lua_State *L) -> int { return 0; });
lua_setfield(L, -2, "hide");
// is_active()
@@ -168,15 +164,11 @@ void registerLuaDialogueApi(lua_State *L)
lua_setfield(L, -2, "is_active");
// select_choice(index)
lua_pushcfunction(L, [](lua_State *L) -> int {
return 0;
});
lua_pushcfunction(L, [](lua_State *L) -> int { return 0; });
lua_setfield(L, -2, "select_choice");
// progress()
lua_pushcfunction(L, [](lua_State *L) -> int {
return 0;
});
lua_pushcfunction(L, [](lua_State *L) -> int { return 0; });
lua_setfield(L, -2, "progress");
// get_settings()
@@ -199,9 +191,7 @@ void registerLuaDialogueApi(lua_State *L)
lua_setfield(L, -2, "get_settings");
// set_settings(table)
lua_pushcfunction(L, [](lua_State *L) -> int {
return 0;
});
lua_pushcfunction(L, [](lua_State *L) -> int { return 0; });
lua_setfield(L, -2, "set_settings");
// save_settings(path)
@@ -223,6 +213,143 @@ void registerLuaDialogueApi(lua_State *L)
lua_setglobal(L, "ecs");
}
// ---------------------------------------------------------------------------
// Stub: LuaCharacterClassApi
// ---------------------------------------------------------------------------
void registerLuaCharacterClassApi(lua_State *L)
{
lua_getglobal(L, "ecs");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
lua_newtable(L);
// get_class_names()
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_newtable(L);
return 1;
});
lua_setfield(L, -2, "get_class_names");
// get_stat_names()
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_newtable(L);
return 1;
});
lua_setfield(L, -2, "get_stat_names");
// get_skill_names()
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_newtable(L);
return 1;
});
lua_setfield(L, -2, "get_skill_names");
// get_need_names()
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_newtable(L);
return 1;
});
lua_setfield(L, -2, "get_need_names");
// get_class(name)
lua_pushcfunction(L, [](lua_State *L) -> int { return 0; });
lua_setfield(L, -2, "get_class");
// get_stat_kind(name)
lua_pushcfunction(L, [](lua_State *L) -> int {
const char *name = lua_tostring(L, 1);
if (!name) {
lua_pushstring(L, "unknown");
return 1;
}
lua_pushstring(L, "unknown");
return 1;
});
lua_setfield(L, -2, "get_stat_kind");
// get_level(entity_id)
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_pushinteger(L, 0);
return 1;
});
lua_setfield(L, -2, "get_level");
// get_xp(entity_id)
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_pushinteger(L, 0);
return 1;
});
lua_setfield(L, -2, "get_xp");
// add_xp(entity_id, amount)
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_pushboolean(L, 0);
return 1;
});
lua_setfield(L, -2, "add_xp");
// get_stat(entity_id, stat_name)
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_pushinteger(L, 0);
return 1;
});
lua_setfield(L, -2, "get_stat");
// get_skill(entity_id, skill_name)
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_pushinteger(L, 0);
return 1;
});
lua_setfield(L, -2, "get_skill");
// get_need(entity_id, need_name)
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_pushinteger(L, 0);
return 1;
});
lua_setfield(L, -2, "get_need");
// get_available_points(entity_id)
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_pushinteger(L, 0);
return 1;
});
lua_setfield(L, -2, "get_available_points");
// set_need(entity_id, need_name, value)
lua_pushcfunction(L, [](lua_State *L) -> int { return 0; });
lua_setfield(L, -2, "set_need");
// get_pool_current(entity_id, pool_name)
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_pushinteger(L, 0);
return 1;
});
lua_setfield(L, -2, "get_pool_current");
// get_pool_max(entity_id, pool_name)
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_pushinteger(L, 0);
return 1;
});
lua_setfield(L, -2, "get_pool_max");
// set_pool_current(entity_id, pool_name, value)
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_pushboolean(L, 0);
return 1;
});
lua_setfield(L, -2, "set_pool_current");
lua_setfield(L, -2, "character_class");
lua_setglobal(L, "ecs");
}
// ---------------------------------------------------------------------------
// Stub: LuaItemApi
// ---------------------------------------------------------------------------
@@ -246,7 +373,8 @@ struct StubInvSlot {
};
static std::unordered_map<int, std::vector<StubInvSlot> > s_stubInventories;
static std::unordered_map<std::string, std::vector<StubInvSlot> > s_stubContainerStates;
static std::unordered_map<std::string, std::vector<StubInvSlot> >
s_stubContainerStates;
void registerLuaItemApi(lua_State *L)
{
@@ -261,7 +389,8 @@ void registerLuaItemApi(lua_State *L)
// items.register
lua_pushcfunction(L, [](lua_State *L) -> int {
if (lua_gettop(L) < 2 || !lua_isstring(L, 1) || !lua_istable(L, 2))
if (lua_gettop(L) < 2 || !lua_isstring(L, 1) ||
!lua_istable(L, 2))
return 0;
std::string itemId = lua_tostring(L, 1);
StubItemDef def;
@@ -337,10 +466,9 @@ void registerLuaItemApi(lua_State *L)
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);
lua_pushboolean(
L,
(it != s_stubItems.end() && it->second.unique) ? 1 : 0);
return 1;
});
lua_setfield(L, -2, "is_unique");
@@ -387,14 +515,16 @@ void registerLuaItemApi(lua_State *L)
if (it != s_stubInventories.end()) {
for (auto &slot : it->second) {
if (slot.itemId == itemId) {
int rem = std::min(count, slot.stackSize);
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(),
std::remove_if(it->second.begin(),
it->second.end(),
[](const StubInvSlot &s) {
return s.stackSize <= 0;
}),
@@ -416,7 +546,8 @@ void registerLuaItemApi(lua_State *L)
auto it = s_stubInventories.find(entityId);
if (it != s_stubInventories.end()) {
for (const auto &slot : it->second) {
if (slot.itemId == itemId && slot.stackSize > 0) {
if (slot.itemId == itemId &&
slot.stackSize > 0) {
has = true;
break;
}
@@ -483,11 +614,14 @@ void registerLuaItemApi(lua_State *L)
lua_rawgeti(L, 2, i);
if (lua_istable(L, -1)) {
StubInvSlot slot;
if (lua_getfield(L, -1, "itemId"), lua_isstring(L, -1))
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);
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);
@@ -538,11 +672,14 @@ void registerLuaItemApi(lua_State *L)
lua_rawgeti(L, 2, i);
if (lua_istable(L, -1)) {
StubInvSlot slot;
if (lua_getfield(L, -1, "itemId"), lua_isstring(L, -1))
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);
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);
@@ -661,9 +798,8 @@ void registerLuaCharacterApi(lua_State *L)
lua_setfield(L, -2, "sex");
lua_pushboolean(L, it->second.persistent ? 1 : 0);
lua_setfield(L, -2, "persistent");
lua_pushinteger(L,
static_cast<lua_Integer>(
it->second.pregnantByFatherId));
lua_pushinteger(L, static_cast<lua_Integer>(
it->second.pregnantByFatherId));
lua_setfield(L, -2, "pregnantByFatherId");
lua_pushnumber(L, it->second.pregnancyProgress);
lua_setfield(L, -2, "pregnancyProgress");
@@ -735,21 +871,18 @@ void registerLuaCharacterApi(lua_State *L)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t id = static_cast<uint64_t>(lua_tointeger(L, 1));
auto it = s_stubCharacters.find(id);
lua_pushboolean(L,
(it != s_stubCharacters.end() &&
it->second.spawned) ?
1 :
0);
lua_pushboolean(L, (it != s_stubCharacters.end() &&
it->second.spawned) ?
1 :
0);
return 1;
});
lua_setfield(L, -2, "is_spawned");
// conceive(motherId, fatherId)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t motherId =
static_cast<uint64_t>(lua_tointeger(L, 1));
uint64_t fatherId =
static_cast<uint64_t>(lua_tointeger(L, 2));
uint64_t motherId = static_cast<uint64_t>(lua_tointeger(L, 1));
uint64_t fatherId = static_cast<uint64_t>(lua_tointeger(L, 2));
auto it = s_stubCharacters.find(motherId);
if (it == s_stubCharacters.end()) {
lua_pushboolean(L, 0);
@@ -765,8 +898,7 @@ void registerLuaCharacterApi(lua_State *L)
// abort_pregnancy(motherId)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t motherId =
static_cast<uint64_t>(lua_tointeger(L, 1));
uint64_t motherId = static_cast<uint64_t>(lua_tointeger(L, 1));
auto it = s_stubCharacters.find(motherId);
if (it != s_stubCharacters.end()) {
it->second.pregnantByFatherId = 0;
@@ -779,22 +911,19 @@ void registerLuaCharacterApi(lua_State *L)
// is_pregnant(motherId)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t motherId =
static_cast<uint64_t>(lua_tointeger(L, 1));
uint64_t motherId = static_cast<uint64_t>(lua_tointeger(L, 1));
auto it = s_stubCharacters.find(motherId);
lua_pushboolean(L,
(it != s_stubCharacters.end() &&
it->second.pregnantByFatherId != 0) ?
1 :
0);
lua_pushboolean(L, (it != s_stubCharacters.end() &&
it->second.pregnantByFatherId != 0) ?
1 :
0);
return 1;
});
lua_setfield(L, -2, "is_pregnant");
// get_pregnancy_progress(motherId)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t motherId =
static_cast<uint64_t>(lua_tointeger(L, 1));
uint64_t motherId = static_cast<uint64_t>(lua_tointeger(L, 1));
auto it = s_stubCharacters.find(motherId);
if (it == s_stubCharacters.end() ||
it->second.pregnantByFatherId == 0) {
@@ -807,11 +936,10 @@ void registerLuaCharacterApi(lua_State *L)
lua_pushnumber(L, it->second.pregnancyMaxProgress);
lua_setfield(L, -2, "maxProgress");
lua_pushnumber(L,
it->second.pregnancyMaxProgress > 0.0f ?
it->second.pregnancyProgress /
it->second
.pregnancyMaxProgress :
0.0f);
it->second.pregnancyMaxProgress > 0.0f ?
it->second.pregnancyProgress /
it->second.pregnancyMaxProgress :
0.0f);
lua_setfield(L, -2, "ratio");
return 1;
});
@@ -819,10 +947,8 @@ void registerLuaCharacterApi(lua_State *L)
// create_child(parentA, parentB)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t parentA =
static_cast<uint64_t>(lua_tointeger(L, 1));
uint64_t parentB =
static_cast<uint64_t>(lua_tointeger(L, 2));
uint64_t parentA = static_cast<uint64_t>(lua_tointeger(L, 1));
uint64_t parentB = static_cast<uint64_t>(lua_tointeger(L, 2));
uint64_t childId = s_stubNextCharId++;
StubCharacter c;
c.id = childId;
@@ -844,18 +970,14 @@ void registerLuaCharacterApi(lua_State *L)
// get_parents(childId)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t childId =
static_cast<uint64_t>(lua_tointeger(L, 1));
uint64_t childId = static_cast<uint64_t>(lua_tointeger(L, 1));
lua_newtable(L);
auto it = s_stubParents.find(childId);
if (it != s_stubParents.end()) {
for (size_t i = 0; i < it->second.size(); i++) {
lua_pushinteger(
L,
static_cast<lua_Integer>(
it->second[i]));
lua_rawseti(L, -2,
static_cast<int>(i + 1));
lua_pushinteger(L, static_cast<lua_Integer>(
it->second[i]));
lua_rawseti(L, -2, static_cast<int>(i + 1));
}
}
return 1;
@@ -864,18 +986,14 @@ void registerLuaCharacterApi(lua_State *L)
// get_children(parentId)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t parentId =
static_cast<uint64_t>(lua_tointeger(L, 1));
uint64_t parentId = static_cast<uint64_t>(lua_tointeger(L, 1));
lua_newtable(L);
auto it = s_stubChildren.find(parentId);
if (it != s_stubChildren.end()) {
for (size_t i = 0; i < it->second.size(); i++) {
lua_pushinteger(
L,
static_cast<lua_Integer>(
it->second[i]));
lua_rawseti(L, -2,
static_cast<int>(i + 1));
lua_pushinteger(L, static_cast<lua_Integer>(
it->second[i]));
lua_rawseti(L, -2, static_cast<int>(i + 1));
}
}
return 1;
+42
View File
@@ -77,6 +77,48 @@ bool ItemEditor::renderComponent(flecs::entity entity, ItemComponent &item)
modified = true;
}
// Action (GOAP action name executed on interact)
char actionBuf[256];
strncpy(actionBuf, item.action.c_str(), sizeof(actionBuf));
actionBuf[sizeof(actionBuf) - 1] = '\0';
if (ImGui::InputText("Action", actionBuf, sizeof(actionBuf))) {
item.action = actionBuf;
modified = true;
}
ImGui::SameLine();
ImGui::TextDisabled("(?)");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Optional GOAP action name.\n"
"If set, pressing E executes this action instead of picking up.");
}
// Instance ID (for global state tracking)
char instanceBuf[256];
strncpy(instanceBuf, item.instanceId.c_str(), sizeof(instanceBuf));
instanceBuf[sizeof(instanceBuf) - 1] = '\0';
if (ImGui::InputText("Instance ID", instanceBuf, sizeof(instanceBuf))) {
item.instanceId = instanceBuf;
modified = true;
}
ImGui::SameLine();
ImGui::TextDisabled("(?)");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Unique ID for save/load tracking.\n"
"If set, disabled state persists across sessions.");
}
// Disabled state
if (ImGui::Checkbox("Disabled", &item.disabled)) {
modified = true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Item has been picked up / consumed.\n"
"Disabled items are hidden in game mode.");
}
ImGui::PopID();
return modified;
}