From 8630bfcf18aeec252cff27fe0c8d6e6d0b3b9112 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Thu, 14 May 2026 13:32:32 +0300 Subject: [PATCH] Updated APIs and tests --- src/features/editScene/CMakeLists.txt | 34 + src/features/editScene/EditorApp.cpp | 3 + src/features/editScene/components/Item.hpp | 16 +- .../lua-examples/character_class_example.lua | 262 +++++++ .../lua-examples/character_example.lua | 284 ++++++++ .../lua-examples/container_example.lua | 199 ++++++ .../lua-examples/dialogue_example.lua | 211 ++++++ .../lua-examples/inventory_example.lua | 198 ++++++ .../lua-examples/item_registry_example.lua | 144 ++++ .../lua-examples/quest_reward_example.lua | 248 +++++++ .../editScene/lua/LuaComponentApi.cpp | 15 + .../editScene/systems/ActuatorSystem.cpp | 59 +- .../editScene/systems/ActuatorSystem.hpp | 3 + .../editScene/systems/BehaviorTreeSystem.cpp | 47 ++ .../editScene/systems/ItemStateRegistry.cpp | 109 +++ .../editScene/systems/ItemStateRegistry.hpp | 55 ++ .../editScene/systems/SceneSerializer.cpp | 7 + .../tests/character_class_lua_test.cpp | 656 ++++++++++++++++++ .../editScene/tests/component_lua_test.cpp | 20 +- .../editScene/tests/dialogue_lua_test.cpp | 432 ++++++++++++ .../editScene/tests/lua_test_stubs.cpp | 274 +++++--- src/features/editScene/ui/ItemEditor.cpp | 42 ++ 22 files changed, 3228 insertions(+), 90 deletions(-) create mode 100644 src/features/editScene/lua-examples/character_class_example.lua create mode 100644 src/features/editScene/lua-examples/character_example.lua create mode 100644 src/features/editScene/lua-examples/container_example.lua create mode 100644 src/features/editScene/lua-examples/dialogue_example.lua create mode 100644 src/features/editScene/lua-examples/inventory_example.lua create mode 100644 src/features/editScene/lua-examples/item_registry_example.lua create mode 100644 src/features/editScene/lua-examples/quest_reward_example.lua create mode 100644 src/features/editScene/systems/ItemStateRegistry.cpp create mode 100644 src/features/editScene/systems/ItemStateRegistry.hpp create mode 100644 src/features/editScene/tests/character_class_lua_test.cpp create mode 100644 src/features/editScene/tests/dialogue_lua_test.cpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 39ee009..102fd78 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -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" diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index ffd8d50..eec2b78 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -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"); diff --git a/src/features/editScene/components/Item.hpp b/src/features/editScene/components/Item.hpp index 1343527..684438e 100644 --- a/src/features/editScene/components/Item.hpp +++ b/src/features/editScene/components/Item.hpp @@ -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) diff --git a/src/features/editScene/lua-examples/character_class_example.lua b/src/features/editScene/lua-examples/character_class_example.lua new file mode 100644 index 0000000..b2efb69 --- /dev/null +++ b/src/features/editScene/lua-examples/character_class_example.lua @@ -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!") diff --git a/src/features/editScene/lua-examples/character_example.lua b/src/features/editScene/lua-examples/character_example.lua new file mode 100644 index 0000000..d8c265a --- /dev/null +++ b/src/features/editScene/lua-examples/character_example.lua @@ -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!") diff --git a/src/features/editScene/lua-examples/container_example.lua b/src/features/editScene/lua-examples/container_example.lua new file mode 100644 index 0000000..714288a --- /dev/null +++ b/src/features/editScene/lua-examples/container_example.lua @@ -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!") diff --git a/src/features/editScene/lua-examples/dialogue_example.lua b/src/features/editScene/lua-examples/dialogue_example.lua new file mode 100644 index 0000000..a751797 --- /dev/null +++ b/src/features/editScene/lua-examples/dialogue_example.lua @@ -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!") diff --git a/src/features/editScene/lua-examples/inventory_example.lua b/src/features/editScene/lua-examples/inventory_example.lua new file mode 100644 index 0000000..57103a4 --- /dev/null +++ b/src/features/editScene/lua-examples/inventory_example.lua @@ -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!") diff --git a/src/features/editScene/lua-examples/item_registry_example.lua b/src/features/editScene/lua-examples/item_registry_example.lua new file mode 100644 index 0000000..bacb844 --- /dev/null +++ b/src/features/editScene/lua-examples/item_registry_example.lua @@ -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!") diff --git a/src/features/editScene/lua-examples/quest_reward_example.lua b/src/features/editScene/lua-examples/quest_reward_example.lua new file mode 100644 index 0000000..cd9552c --- /dev/null +++ b/src/features/editScene/lua-examples/quest_reward_example.lua @@ -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!") diff --git a/src/features/editScene/lua/LuaComponentApi.cpp b/src/features/editScene/lua/LuaComponentApi.cpp index a0f3148..70bb1fe 100644 --- a/src/features/editScene/lua/LuaComponentApi.cpp +++ b/src/features/editScene/lua/LuaComponentApi.cpp @@ -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 --- diff --git a/src/features/editScene/systems/ActuatorSystem.cpp b/src/features/editScene/systems/ActuatorSystem.cpp index 0171ed2..a08ace7 100644 --- a/src/features/editScene/systems/ActuatorSystem.cpp +++ b/src/features/editScene/systems/ActuatorSystem.cpp @@ -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()) + 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() && input.ePressed) { - // Pick up item - if (m_itemSystem) { - m_itemSystem->pickupItem(playerCharacter, - targetEntity); + auto &item = targetEntity.get_mut(); + + 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; } diff --git a/src/features/editScene/systems/ActuatorSystem.hpp b/src/features/editScene/systems/ActuatorSystem.hpp index 148421f..f8e0333 100644 --- a/src/features/editScene/systems/ActuatorSystem.hpp +++ b/src/features/editScene/systems/ActuatorSystem.hpp @@ -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); diff --git a/src/features/editScene/systems/BehaviorTreeSystem.cpp b/src/features/editScene/systems/BehaviorTreeSystem.cpp index c5d2d61..ef0c4ec 100644 --- a/src/features/editScene/systems/BehaviorTreeSystem.cpp +++ b/src/features/editScene/systems/BehaviorTreeSystem.cpp @@ -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().each( + [&](flecs::entity itemEntity, + ItemComponent &itemComp) { + if (itemComp.instanceId == + node.name.c_str() && + !targetEntity.is_alive()) { + targetEntity = itemEntity; + } + }); + } + + if (!targetEntity.is_alive() || + !targetEntity.has()) { + std::cout << "[BT] disableItem: no item entity" + << std::endl; + return Status::failure; + } + + auto &item = targetEntity.get_mut(); + item.disabled = true; + if (!item.instanceId.empty()) { + ItemStateRegistry::getInstance().setDisabled( + item.instanceId, true); + } + if (targetEntity.has()) { + 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. diff --git a/src/features/editScene/systems/ItemStateRegistry.cpp b/src/features/editScene/systems/ItemStateRegistry.cpp new file mode 100644 index 0000000..78a05b8 --- /dev/null +++ b/src/features/editScene/systems/ItemStateRegistry.cpp @@ -0,0 +1,109 @@ +#include "ItemStateRegistry.hpp" +#include +#include + +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); +} diff --git a/src/features/editScene/systems/ItemStateRegistry.hpp b/src/features/editScene/systems/ItemStateRegistry.hpp new file mode 100644 index 0000000..03ecce9 --- /dev/null +++ b/src/features/editScene/systems/ItemStateRegistry.hpp @@ -0,0 +1,55 @@ +#ifndef EDITSCENE_ITEMSTATEREGISTRY_HPP +#define EDITSCENE_ITEMSTATEREGISTRY_HPP +#pragma once + +#include +#include +#include + +/** + * 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 m_states; + mutable std::string m_lastError; + std::string m_autoSavePath; +}; + +#endif // EDITSCENE_ITEMSTATEREGISTRY_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 1f95064..35e863b 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -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; } diff --git a/src/features/editScene/tests/character_class_lua_test.cpp b/src/features/editScene/tests/character_class_lua_test.cpp new file mode 100644 index 0000000..78189d7 --- /dev/null +++ b/src/features/editScene/tests/character_class_lua_test.cpp @@ -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 +#include +#include +#include +#include + +// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager) +#include "ogre_stub.h" + +// Include Lua +extern "C" { +#include +#include +#include +} + +// Forward declare the registration function +namespace editScene +{ +void 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; +} diff --git a/src/features/editScene/tests/component_lua_test.cpp b/src/features/editScene/tests/component_lua_test.cpp index 2ca6961..fcc636d 100644 --- a/src/features/editScene/tests/component_lua_test.cpp +++ b/src/features/editScene/tests/component_lua_test.cpp @@ -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"); diff --git a/src/features/editScene/tests/dialogue_lua_test.cpp b/src/features/editScene/tests/dialogue_lua_test.cpp new file mode 100644 index 0000000..c20cb17 --- /dev/null +++ b/src/features/editScene/tests/dialogue_lua_test.cpp @@ -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 +#include +#include +#include +#include + +// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager) +#include "ogre_stub.h" + +// Include Lua +extern "C" { +#include +#include +#include +} + +// Forward declare the registration function +namespace editScene +{ +void 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; +} diff --git a/src/features/editScene/tests/lua_test_stubs.cpp b/src/features/editScene/tests/lua_test_stubs.cpp index a811b4a..1b1472c 100644 --- a/src/features/editScene/tests/lua_test_stubs.cpp +++ b/src/features/editScene/tests/lua_test_stubs.cpp @@ -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 > s_stubInventories; -static std::unordered_map > s_stubContainerStates; +static std::unordered_map > + 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( - it->second.pregnantByFatherId)); + lua_pushinteger(L, static_cast( + 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(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(lua_tointeger(L, 1)); - uint64_t fatherId = - static_cast(lua_tointeger(L, 2)); + uint64_t motherId = static_cast(lua_tointeger(L, 1)); + uint64_t fatherId = static_cast(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(lua_tointeger(L, 1)); + uint64_t motherId = static_cast(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(lua_tointeger(L, 1)); + uint64_t motherId = static_cast(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(lua_tointeger(L, 1)); + uint64_t motherId = static_cast(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(lua_tointeger(L, 1)); - uint64_t parentB = - static_cast(lua_tointeger(L, 2)); + uint64_t parentA = static_cast(lua_tointeger(L, 1)); + uint64_t parentB = static_cast(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(lua_tointeger(L, 1)); + uint64_t childId = static_cast(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( - it->second[i])); - lua_rawseti(L, -2, - static_cast(i + 1)); + lua_pushinteger(L, static_cast( + it->second[i])); + lua_rawseti(L, -2, static_cast(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(lua_tointeger(L, 1)); + uint64_t parentId = static_cast(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( - it->second[i])); - lua_rawseti(L, -2, - static_cast(i + 1)); + lua_pushinteger(L, static_cast( + it->second[i])); + lua_rawseti(L, -2, static_cast(i + 1)); } } return 1; diff --git a/src/features/editScene/ui/ItemEditor.cpp b/src/features/editScene/ui/ItemEditor.cpp index bfdf275..6dc29d9 100644 --- a/src/features/editScene/ui/ItemEditor.cpp +++ b/src/features/editScene/ui/ItemEditor.cpp @@ -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; }