From ef49506515f2e6ab9551453e79372761c5498aca Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Wed, 13 May 2026 23:31:59 +0300 Subject: [PATCH] Pregnancy and birth --- src/features/editScene/CMakeLists.txt | 20 + src/features/editScene/EditorApp.cpp | 10 + src/features/editScene/EditorApp.hpp | 6 + .../editScene/lua/LuaCharacterApi.cpp | 322 +++++++ .../editScene/lua/LuaCharacterApi.hpp | 11 + .../editScene/systems/CharacterRegistry.cpp | 878 +++++++++++++++++- .../editScene/systems/CharacterRegistry.hpp | 103 +- .../editScene/systems/PregnancySystem.cpp | 79 ++ .../editScene/systems/PregnancySystem.hpp | 28 + .../editScene/tests/character_lua_test.cpp | 526 +++++++++++ .../editScene/tests/lua_test_stubs.cpp | 318 +++++++ 11 files changed, 2248 insertions(+), 53 deletions(-) create mode 100644 src/features/editScene/lua/LuaCharacterApi.cpp create mode 100644 src/features/editScene/lua/LuaCharacterApi.hpp create mode 100644 src/features/editScene/systems/PregnancySystem.cpp create mode 100644 src/features/editScene/systems/PregnancySystem.hpp create mode 100644 src/features/editScene/tests/character_lua_test.cpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index acc9b58..edd34da 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -39,6 +39,7 @@ set(EDITSCENE_SOURCES systems/CharacterSlotSystem.cpp systems/CharacterRegistry.cpp systems/MarkovNameGenerator.cpp + systems/PregnancySystem.cpp systems/AnimationTreeSystem.cpp systems/BehaviorTreeSystem.cpp systems/NavMeshSystem.cpp @@ -163,6 +164,7 @@ set(EDITSCENE_SOURCES lua/LuaBehaviorTreeApi.cpp lua/LuaGameModeApi.cpp lua/LuaCharacterClassApi.cpp + lua/LuaCharacterApi.cpp ) set(EDITSCENE_HEADERS @@ -213,6 +215,7 @@ set(EDITSCENE_HEADERS systems/ProceduralMeshSystem.hpp systems/CharacterSlotSystem.hpp systems/CharacterRegistry.hpp + systems/PregnancySystem.hpp systems/AnimationTreeSystem.hpp systems/BehaviorTreeSystem.hpp systems/NavMeshSystem.hpp @@ -323,6 +326,7 @@ set(EDITSCENE_HEADERS lua/LuaBehaviorTreeApi.hpp lua/LuaGameModeApi.hpp lua/LuaCharacterClassApi.hpp + lua/LuaCharacterApi.hpp ) add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS}) @@ -489,6 +493,22 @@ target_include_directories(game_mode_lua_test PRIVATE ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src ) +# Test: Character Lua API +add_executable(character_lua_test + tests/character_lua_test.cpp + tests/lua_test_stubs.cpp +) + +target_link_libraries(character_lua_test + lua +) + +target_include_directories(character_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 0737887..3614dd7 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -31,7 +31,9 @@ #include "systems/StartupMenuSystem.hpp" #include "systems/DialogueSystem.hpp" #include "systems/CharacterClassSystem.hpp" +#include "systems/PregnancySystem.hpp" #include "components/CharacterClassDatabase.hpp" +#include "lua/LuaCharacterApi.hpp" #include "systems/PlayerControllerSystem.hpp" #include "systems/SceneSerializer.hpp" #include "camera/EditorCamera.hpp" @@ -157,6 +159,11 @@ void ImGuiRenderListener::preViewportUpdate( m_editorApp->getCharacterClassSystem()->update(m_deltaTime); m_editorApp->getCharacterClassSystem()->renderDialogs(); } + + // Pregnancy system (advances pregnancies, triggers birth) + if (m_editorApp && m_editorApp->getPregnancySystem()) { + m_editorApp->getPregnancySystem()->update(m_deltaTime); + } } void ImGuiRenderListener::postViewportUpdate( @@ -452,6 +459,8 @@ void EditorApp::setup() m_characterClassSystem = std::make_unique( m_world, this); + m_pregnancySystem = + std::make_unique(m_world); CharacterClassDatabase::loadFromJson( "character_class.json"); @@ -527,6 +536,7 @@ void EditorApp::setup() editScene::registerLuaBehaviorTreeApi(L); editScene::registerLuaGameModeApi(L); editScene::registerLuaCharacterClassApi(L); + editScene::registerLuaCharacterApi(L); editScene::registerLuaDialogueApi(L); // Run late setup: load data.lua and initial scripts. diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index 1c7cab5..b3a8d23 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -45,6 +45,7 @@ class ActuatorSystem; class EventHandlerSystem; class ItemSystem; class CharacterClassSystem; +class PregnancySystem; class EditorApp; /** @@ -204,6 +205,10 @@ public: { return m_characterClassSystem.get(); } + PregnancySystem *getPregnancySystem() const + { + return m_pregnancySystem.get(); + } Ogre::ImGuiOverlay *getImGuiOverlay() const { return m_imguiOverlay; @@ -251,6 +256,7 @@ private: std::unique_ptr m_eventHandlerSystem; std::unique_ptr m_itemSystem; std::unique_ptr m_characterClassSystem; + std::unique_ptr m_pregnancySystem; // Game systems std::unique_ptr m_startupMenuSystem; diff --git a/src/features/editScene/lua/LuaCharacterApi.cpp b/src/features/editScene/lua/LuaCharacterApi.cpp new file mode 100644 index 0000000..01adf91 --- /dev/null +++ b/src/features/editScene/lua/LuaCharacterApi.cpp @@ -0,0 +1,322 @@ +#include "LuaCharacterApi.hpp" +#include "LuaEntityApi.hpp" +#include "../systems/CharacterRegistry.hpp" +#include "../components/CharacterIdentity.hpp" +#include + +namespace editScene +{ + +// --------------------------------------------------------------------------- +// Helper: get the Flecs world from the Lua registry +// --------------------------------------------------------------------------- + +static flecs::world getWorld(lua_State *L) +{ + lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld"); + OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered"); + flecs::world *world = + static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + return *world; +} + +// --------------------------------------------------------------------------- +// Helper: push uint64_t vector as Lua table +// --------------------------------------------------------------------------- + +static void pushUint64Vector(lua_State *L, const std::vector &vec) +{ + lua_newtable(L); + for (size_t i = 0; i < vec.size(); i++) { + lua_pushinteger(L, static_cast(vec[i])); + lua_rawseti(L, -2, static_cast(i + 1)); + } +} + +// --------------------------------------------------------------------------- +// Character creation & management +// --------------------------------------------------------------------------- + +static int luaCharacterCreate(lua_State *L) +{ + const char *firstName = luaL_checkstring(L, 1); + const char *lastName = luaL_checkstring(L, 2); + const char *templatePath = lua_tostring(L, 3); + bool persistent = true; + if (lua_gettop(L) >= 4) + persistent = lua_toboolean(L, 4) != 0; + + uint64_t id = CharacterRegistry::getSingleton().createCharacter( + firstName, lastName, templatePath ? templatePath : "", + persistent); + lua_pushinteger(L, static_cast(id)); + return 1; +} + +static int luaCharacterDelete(lua_State *L) +{ + uint64_t id = static_cast(luaL_checkinteger(L, 1)); + CharacterRegistry::getSingleton().deleteCharacter(id); + return 0; +} + +static int luaCharacterFind(lua_State *L) +{ + uint64_t id = static_cast(luaL_checkinteger(L, 1)); + const CharacterRegistry::CharacterRecord *c = + CharacterRegistry::getSingleton().findCharacter(id); + if (!c) { + lua_pushnil(L); + return 1; + } + + lua_newtable(L); + lua_pushinteger(L, static_cast(c->id)); + lua_setfield(L, -2, "id"); + lua_pushstring(L, c->firstName.c_str()); + lua_setfield(L, -2, "firstName"); + lua_pushstring(L, c->lastName.c_str()); + lua_setfield(L, -2, "lastName"); + lua_pushstring(L, c->className.c_str()); + lua_setfield(L, -2, "className"); + lua_pushinteger(L, c->level); + lua_setfield(L, -2, "level"); + lua_pushinteger(L, c->ageYears); + lua_setfield(L, -2, "ageYears"); + lua_pushstring(L, c->inlineSex.c_str()); + lua_setfield(L, -2, "sex"); + lua_pushboolean(L, c->persistent ? 1 : 0); + lua_setfield(L, -2, "persistent"); + lua_pushinteger(L, + static_cast(c->pregnantByFatherId)); + lua_setfield(L, -2, "pregnantByFatherId"); + lua_pushnumber(L, c->pregnancyProgress); + lua_setfield(L, -2, "pregnancyProgress"); + lua_pushnumber(L, c->pregnancyMaxProgress); + lua_setfield(L, -2, "pregnancyMaxProgress"); + return 1; +} + +static int luaCharacterGetAll(lua_State *L) +{ + const auto &chars = + CharacterRegistry::getSingleton().getCharacters(); + lua_newtable(L); + int idx = 1; + for (const auto &pair : chars) { + lua_pushinteger(L, static_cast(pair.first)); + lua_rawseti(L, -2, idx++); + } + return 1; +} + +static int luaCharacterSetName(lua_State *L) +{ + uint64_t id = static_cast(luaL_checkinteger(L, 1)); + const char *firstName = luaL_checkstring(L, 2); + const char *lastName = luaL_checkstring(L, 3); + + CharacterRegistry::CharacterRecord *c = + CharacterRegistry::getSingleton().findCharacter(id); + if (c) { + c->firstName = firstName; + c->lastName = lastName; + } + return 0; +} + +// --------------------------------------------------------------------------- +// Spawn / despawn +// --------------------------------------------------------------------------- + +static int luaCharacterSpawn(lua_State *L) +{ + uint64_t id = static_cast(luaL_checkinteger(L, 1)); + flecs::entity e = + CharacterRegistry::getSingleton().spawnCharacter(id); + if (!e.is_alive()) { + lua_pushnil(L); + return 1; + } + int luaId = g_luaEntityIdMap.addEntity(e); + lua_pushinteger(L, luaId); + return 1; +} + +static int luaCharacterDespawn(lua_State *L) +{ + uint64_t id = static_cast(luaL_checkinteger(L, 1)); + bool ok = CharacterRegistry::getSingleton().despawnCharacter(id); + lua_pushboolean(L, ok ? 1 : 0); + return 1; +} + +static int luaCharacterIsSpawned(lua_State *L) +{ + uint64_t id = static_cast(luaL_checkinteger(L, 1)); + flecs::entity e = + CharacterRegistry::getSingleton().findSpawnedEntity(id); + lua_pushboolean(L, e.is_alive() ? 1 : 0); + return 1; +} + +// --------------------------------------------------------------------------- +// Pregnancy +// --------------------------------------------------------------------------- + +static int luaCharacterConceive(lua_State *L) +{ + uint64_t motherId = + static_cast(luaL_checkinteger(L, 1)); + uint64_t fatherId = + static_cast(luaL_checkinteger(L, 2)); + bool ok = CharacterRegistry::getSingleton().conceive(motherId, + fatherId); + lua_pushboolean(L, ok ? 1 : 0); + return 1; +} + +static int luaCharacterAbortPregnancy(lua_State *L) +{ + uint64_t motherId = + static_cast(luaL_checkinteger(L, 1)); + CharacterRegistry::getSingleton().abortPregnancy(motherId); + return 0; +} + +static int luaCharacterIsPregnant(lua_State *L) +{ + uint64_t motherId = + static_cast(luaL_checkinteger(L, 1)); + bool pregnant = CharacterRegistry::getSingleton().isPregnant( + motherId); + lua_pushboolean(L, pregnant ? 1 : 0); + return 1; +} + +static int luaCharacterGetPregnancyProgress(lua_State *L) +{ + uint64_t motherId = + static_cast(luaL_checkinteger(L, 1)); + const CharacterRegistry::CharacterRecord *c = + CharacterRegistry::getSingleton().findCharacter(motherId); + if (!c || c->pregnantByFatherId == 0) { + lua_pushnil(L); + return 1; + } + lua_newtable(L); + lua_pushnumber(L, c->pregnancyProgress); + lua_setfield(L, -2, "progress"); + lua_pushnumber(L, c->pregnancyMaxProgress); + lua_setfield(L, -2, "maxProgress"); + lua_pushnumber(L, + c->pregnancyMaxProgress > 0.0f ? + c->pregnancyProgress / + c->pregnancyMaxProgress : + 0.0f); + lua_setfield(L, -2, "ratio"); + return 1; +} + +// --------------------------------------------------------------------------- +// Birth & lineage +// --------------------------------------------------------------------------- + +static int luaCharacterCreateChild(lua_State *L) +{ + uint64_t parentA = + static_cast(luaL_checkinteger(L, 1)); + uint64_t parentB = + static_cast(luaL_checkinteger(L, 2)); + uint64_t childId = + CharacterRegistry::getSingleton().createChild(parentA, + parentB); + lua_pushinteger(L, static_cast(childId)); + return 1; +} + +static int luaCharacterGetParents(lua_State *L) +{ + uint64_t childId = + static_cast(luaL_checkinteger(L, 1)); + auto parents = CharacterRegistry::getSingleton().getParents( + childId); + pushUint64Vector(L, parents); + return 1; +} + +static int luaCharacterGetChildren(lua_State *L) +{ + uint64_t parentId = + static_cast(luaL_checkinteger(L, 1)); + auto children = CharacterRegistry::getSingleton().getChildren( + parentId); + pushUint64Vector(L, children); + return 1; +} + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +void registerLuaCharacterApi(lua_State *L) +{ + lua_getglobal(L, "ecs"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + + lua_newtable(L); // ecs.character + + lua_pushcfunction(L, luaCharacterCreate); + lua_setfield(L, -2, "create"); + + lua_pushcfunction(L, luaCharacterDelete); + lua_setfield(L, -2, "delete"); + + lua_pushcfunction(L, luaCharacterFind); + lua_setfield(L, -2, "find"); + + lua_pushcfunction(L, luaCharacterGetAll); + lua_setfield(L, -2, "get_all"); + + lua_pushcfunction(L, luaCharacterSetName); + lua_setfield(L, -2, "set_name"); + + lua_pushcfunction(L, luaCharacterSpawn); + lua_setfield(L, -2, "spawn"); + + lua_pushcfunction(L, luaCharacterDespawn); + lua_setfield(L, -2, "despawn"); + + lua_pushcfunction(L, luaCharacterIsSpawned); + lua_setfield(L, -2, "is_spawned"); + + lua_pushcfunction(L, luaCharacterConceive); + lua_setfield(L, -2, "conceive"); + + lua_pushcfunction(L, luaCharacterAbortPregnancy); + lua_setfield(L, -2, "abort_pregnancy"); + + lua_pushcfunction(L, luaCharacterIsPregnant); + lua_setfield(L, -2, "is_pregnant"); + + lua_pushcfunction(L, luaCharacterGetPregnancyProgress); + lua_setfield(L, -2, "get_pregnancy_progress"); + + lua_pushcfunction(L, luaCharacterCreateChild); + lua_setfield(L, -2, "create_child"); + + lua_pushcfunction(L, luaCharacterGetParents); + lua_setfield(L, -2, "get_parents"); + + lua_pushcfunction(L, luaCharacterGetChildren); + lua_setfield(L, -2, "get_children"); + + lua_setfield(L, -2, "character"); // ecs.character = { ... } + lua_setglobal(L, "ecs"); +} + +} // namespace editScene diff --git a/src/features/editScene/lua/LuaCharacterApi.hpp b/src/features/editScene/lua/LuaCharacterApi.hpp new file mode 100644 index 0000000..29049e6 --- /dev/null +++ b/src/features/editScene/lua/LuaCharacterApi.hpp @@ -0,0 +1,11 @@ +#ifndef EDITSCENE_LUA_CHARACTER_API_HPP +#define EDITSCENE_LUA_CHARACTER_API_HPP +#pragma once + +#include + +namespace editScene { +void registerLuaCharacterApi(lua_State *L); +} + +#endif // EDITSCENE_LUA_CHARACTER_API_HPP diff --git a/src/features/editScene/systems/CharacterRegistry.cpp b/src/features/editScene/systems/CharacterRegistry.cpp index aa955cc..daf0e62 100644 --- a/src/features/editScene/systems/CharacterRegistry.cpp +++ b/src/features/editScene/systems/CharacterRegistry.cpp @@ -4,7 +4,11 @@ #include "../components/CharacterIdentity.hpp" #include "../components/CharacterSlots.hpp" #include "../components/CharacterClassDatabase.hpp" +#include "../components/Transform.hpp" +#include "../components/EntityName.hpp" +#include "../components/EditorMarker.hpp" #include +#include #include #include #include @@ -177,25 +181,64 @@ void CharacterRegistry::scanTemplates() /* Name Generation */ /* ===================================================================== */ -void CharacterRegistry::learnNamesFromRegistry() +static std::string getCharacterSexForLearning(const CharacterRegistry::CharacterRecord &c) { - m_firstNameGen.clear(); + if (c.inlineSex == "female") + return "female"; + /* If inlineSex is default male, try prefab for accuracy */ + if (c.inlineSex == "male" && + std::filesystem::exists(c.prefabPath)) { + std::string age, sex; + int outfit; + if (CharacterRegistry::readPrefabSlots(c.prefabPath, age, sex, + outfit) && + sex == "female") + return "female"; + } + return "male"; +} + +void CharacterRegistry::rebuildNameGenerators() +{ + m_maleFirstNameGen.clear(); + m_femaleFirstNameGen.clear(); m_lastNameGen.clear(); + + /* Feed seeded names first */ + for (const auto &name : m_maleFirstNameSeeds) + m_maleFirstNameGen.learn(name); + for (const auto &name : m_femaleFirstNameSeeds) + m_femaleFirstNameGen.learn(name); + for (const auto &name : m_lastNameSeeds) + m_lastNameGen.learn(name); + + /* Then feed registered character names */ for (const auto &pair : m_characters) { const CharacterRecord &c = pair.second; - if (!c.firstName.empty()) - m_firstNameGen.learn(c.firstName); + if (!c.firstName.empty()) { + if (getCharacterSexForLearning(c) == "female") + m_femaleFirstNameGen.learn(c.firstName); + else + m_maleFirstNameGen.learn(c.firstName); + } if (!c.lastName.empty()) m_lastNameGen.learn(c.lastName); } } -std::string CharacterRegistry::generateFirstName() const +void CharacterRegistry::learnNamesFromRegistry() +{ + rebuildNameGenerators(); +} + +std::string CharacterRegistry::generateFirstName(const std::string &sex) const { std::unordered_set existing; for (const auto &pair : m_characters) existing.insert(pair.second.firstName); - return m_firstNameGen.generate(3, 12, &existing); + if (sex == "female") + return m_femaleFirstNameGen.generate(3, 12, &existing); + return m_maleFirstNameGen.generate(3, 12, &existing); } std::string CharacterRegistry::generateLastName() const @@ -206,6 +249,304 @@ std::string CharacterRegistry::generateLastName() const return m_lastNameGen.generate(3, 12, &existing); } +/* ------------------------------------------------------------------ */ +/* Family / Birth */ +/* ------------------------------------------------------------------ */ + +std::vector CharacterRegistry::getParents(uint64_t childId) const +{ + std::vector result; + auto range = m_relByTarget.equal_range(childId); + for (auto it = range.first; it != range.second; ++it) { + const Relationship &r = m_relationships[it->second]; + if (!r.sourceIsGroup && r.tags.find("parent") != r.tags.end()) + result.push_back(r.sourceId); + } + return result; +} + +std::vector CharacterRegistry::getChildren(uint64_t parentId) const +{ + std::vector result; + auto range = m_relBySource.equal_range(parentId); + for (auto it = range.first; it != range.second; ++it) { + const Relationship &r = m_relationships[it->second]; + if (!r.targetIsGroup && r.tags.find("parent") != r.tags.end()) + result.push_back(r.targetId); + } + return result; +} + +/* ------------------------------------------------------------------ */ +/* Prefab appearance reader */ +/* ------------------------------------------------------------------ */ + +static bool readPrefabAppearance(const std::string &path, + CharacterSlotsComponent &cs, + std::unordered_map &shapeKeys) +{ + try { + std::ifstream file(path); + if (!file.is_open()) + return false; + nlohmann::json j; + file >> j; + if (j.contains("characterSlots")) { + auto &s = j["characterSlots"]; + cs.age = s.value("age", "adult"); + cs.sex = s.value("sex", "male"); + cs.outfitLevel = s.value("outfitLevel", 2); + if (s.contains("slotSelections")) { + for (auto &[slot, selJson] : s["slotSelections"].items()) { + SlotSelection sel; + sel.layer1Mesh = selJson.value("layer1Mesh", ""); + sel.layer2Mesh = selJson.value("layer2Mesh", ""); + sel.explicitMesh = selJson.value("explicitMesh", ""); + cs.slotSelections[slot] = sel; + } + } + } + if (j.contains("characterShapeKeys")) { + auto &sk = j["characterShapeKeys"]; + if (sk.contains("weights")) { + for (auto &[k, v] : sk["weights"].items()) + shapeKeys[k] = v.get(); + } + } + return true; + } catch (...) { + return false; + } +} + +uint64_t CharacterRegistry::createChild(uint64_t parentA, uint64_t parentB) +{ + const CharacterRecord *pA = findCharacter(parentA); + const CharacterRecord *pB = findCharacter(parentB); + if (!pA || !pB || parentA == parentB) + return 0; + + /* Ensure generators are up to date */ + learnNamesFromRegistry(); + + /* Determine actual sex of both parents (prefab overrides inline) */ + std::string sexA = pA->inlineSex; + std::string sexB = pB->inlineSex; + if (std::filesystem::exists(pA->prefabPath)) { + std::string a, s; + int o; + if (readPrefabSlots(pA->prefabPath, a, s, o)) + sexA = s; + } + if (std::filesystem::exists(pB->prefabPath)) { + std::string a, s; + int o; + if (readPrefabSlots(pB->prefabPath, a, s, o)) + sexB = s; + } + + /* Random sex for child */ + std::string childSex = (rand() % 2 == 0) ? "male" : "female"; + + std::string first = generateFirstName(childSex); + if (first.empty()) + first = "Child"; + + uint64_t id = createCharacter(first, pA->lastName, "", false); + CharacterRecord *child = findCharacter(id); + if (!child) + return 0; + + child->ageYears = 0; + child->inlineSex = childSex; + const CharacterRecord *sameSexParent = + (child->inlineSex == sexA) ? pA : pB; + + child->inlineAge = "adult"; /* default; can be changed */ + child->inlineOutfitLevel = 2; + + /* Inherit class from same-sex parent, fallback to other */ + if (!sameSexParent->className.empty()) + child->className = sameSexParent->className; + else if (!pA->className.empty()) + child->className = pA->className; + else if (!pB->className.empty()) + child->className = pB->className; + + /* Base stats/skills/needs from class */ + initializeFromClass(id); + + /* Stat remixing from both parents */ + auto &db = CharacterClassDatabase::getSingleton(); + for (const auto &name : db.getStatNames()) { + int valA = 0, valB = 0; + auto itA = pA->stats.find(name.c_str()); + if (itA != pA->stats.end()) + valA = itA->second; + auto itB = pB->stats.find(name.c_str()); + if (itB != pB->stats.end()) + valB = itB->second; + + /* Randomly pick from one parent, then DNA variation */ + int base = (rand() % 2 == 0) ? valA : valB; + int dna = (rand() % 5) - 2; /* -2 .. +2 */ + int result = base + dna; + + /* Clamp to class base minimum */ + const auto *cls = db.findClass(child->className); + if (cls) { + auto itBase = cls->baseStats.find(name.c_str()); + if (itBase != cls->baseStats.end() && result < itBase->second) + result = itBase->second; + } + const auto *statDef = db.findStat(name); + if (statDef) { + if (result < statDef->minValue) + result = statDef->minValue; + if (result > statDef->maxValue) + result = statDef->maxValue; + } + + child->stats[name.c_str()] = result; + } + + /* Needs: class base (already set by initializeFromClass) */ + /* Skills: class base (already set by initializeFromClass) */ + + /* Copy appearance from same-sex parent */ + if (sameSexParent->inlineSlotSelections.empty() && + std::filesystem::exists(sameSexParent->prefabPath)) { + CharacterSlotsComponent tmpCs; + readPrefabAppearance(sameSexParent->prefabPath, tmpCs, + child->inlineShapeKeyWeights); + child->inlineAge = tmpCs.age; + child->inlineSex = tmpCs.sex; + child->inlineOutfitLevel = tmpCs.outfitLevel; + child->inlineSlotSelections = tmpCs.slotSelections; + } else { + child->inlineAge = sameSexParent->inlineAge; + child->inlineSex = sameSexParent->inlineSex; + child->inlineOutfitLevel = sameSexParent->inlineOutfitLevel; + child->inlineSlotSelections = sameSexParent->inlineSlotSelections; + child->inlineShapeKeyWeights = sameSexParent->inlineShapeKeyWeights; + } + + /* Randomize configured birth shape keys */ + if (!m_birthRandomizableShapeKeys.empty()) { + for (const auto &key : m_birthRandomizableShapeKeys) { + /* Skip excluded keys */ + if (std::find(m_birthExcludedShapeKeys.begin(), + m_birthExcludedShapeKeys.end(), + key) != m_birthExcludedShapeKeys.end()) + continue; + float r = static_cast(rand()) / RAND_MAX; + child->inlineShapeKeyWeights[key] = r; + } + } + + /* Relationships */ + addRelationshipTag(parentA, false, id, false, "parent"); + addRelationshipTag(id, false, parentA, false, "child"); + addRelationshipTag(parentB, false, id, false, "parent"); + addRelationshipTag(id, false, parentB, false, "child"); + + autoSave(); + learnNamesFromRegistry(); + return id; +} + +/* ------------------------------------------------------------------ */ +/* Pregnancy helpers */ +/* ------------------------------------------------------------------ */ + +bool CharacterRegistry::conceive(uint64_t motherId, uint64_t fatherId) +{ + CharacterRecord *mother = findCharacter(motherId); + CharacterRecord *father = findCharacter(fatherId); + if (!mother || !father || motherId == fatherId) + return false; + if (mother->pregnantByFatherId != 0) + return false; + + mother->pregnantByFatherId = fatherId; + mother->pregnancyProgress = 0.0f; + + float base = (mother->basePregnancyDuration > 0.0f) ? + mother->basePregnancyDuration : + m_basePregnancyDuration; + float randomFactor = 0.8f + static_cast(rand() % 41) / 100.0f; + mother->pregnancyMaxProgress = base * randomFactor; + + autoSave(); + return true; +} + +void CharacterRegistry::abortPregnancy(uint64_t motherId) +{ + CharacterRecord *mother = findCharacter(motherId); + if (!mother) + return; + mother->pregnantByFatherId = 0; + mother->pregnancyProgress = 0.0f; + mother->pregnancyMaxProgress = 0.0f; + autoSave(); +} + +bool CharacterRegistry::isPregnant(uint64_t motherId) const +{ + const CharacterRecord *mother = findCharacter(motherId); + if (!mother) + return false; + return mother->pregnantByFatherId != 0; +} + +/* ------------------------------------------------------------------ */ +/* Inline spawn (no prefab file) */ +/* ------------------------------------------------------------------ */ + +flecs::entity CharacterRegistry::spawnInlineCharacter(const CharacterRecord &c, + const Ogre::Vector3 &pos) +{ + if (!m_world || !m_sceneMgr) + return flecs::entity::null(); + + flecs::entity inst = m_world->entity(); + inst.add(); + inst.set(EntityNameComponent( + c.firstName + " " + c.lastName)); + + /* Transform */ + Ogre::SceneNode *node = + m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + node->setPosition(pos); + TransformComponent transform; + transform.node = node; + transform.position = pos; + transform.rotation = Ogre::Quaternion::IDENTITY; + transform.scale = Ogre::Vector3(1, 1, 1); + transform.applyToNode(); + inst.set(transform); + + /* CharacterSlots */ + CharacterSlotsComponent cs; + cs.age = c.inlineAge; + cs.sex = c.inlineSex; + cs.outfitLevel = c.inlineOutfitLevel; + cs.slotSelections = c.inlineSlotSelections; + cs.dirty = true; + inst.set(cs); + + /* Shape keys */ + if (!c.inlineShapeKeyWeights.empty()) { + CharacterShapeKeysComponent skc; + skc.weights = c.inlineShapeKeyWeights; + skc.dirty = true; + inst.set(skc); + } + + return inst; +} + /* ===================================================================== */ /* Spawn / Save */ /* ===================================================================== */ @@ -366,11 +707,16 @@ flecs::entity CharacterRegistry::spawnCharacter(uint64_t id) if (existing.is_alive()) existing.destruct(); - PrefabSystem prefabSys(*m_world, m_sceneMgr); Ogre::Vector3 pos(0, 0, 0); - flecs::entity inst = prefabSys.createInstance( - c->prefabPath, flecs::entity::null(), pos, - c->firstName + " " + c->lastName, m_uiSystem); + flecs::entity inst; + if (std::filesystem::exists(c->prefabPath)) { + PrefabSystem prefabSys(*m_world, m_sceneMgr); + inst = prefabSys.createInstance( + c->prefabPath, flecs::entity::null(), pos, + c->firstName + " " + c->lastName, m_uiSystem); + } else { + inst = spawnInlineCharacter(*c, pos); + } if (inst.is_alive()) { inst.set( @@ -453,23 +799,28 @@ void CharacterRegistry::removeFromIndex(uint64_t sourceId, uint64_t targetId, /* ===================================================================== */ uint64_t CharacterRegistry::createCharacter(const std::string &firstName, - const std::string &lastName, - const std::string &templatePath) + const std::string &lastName, + const std::string &templatePath, + bool persistent) { - uint64_t id = m_nextId++; + uint64_t id = persistent ? m_nextId++ : m_nextRuntimeId++; CharacterRecord rec; rec.id = id; rec.firstName = firstName; rec.lastName = lastName; - rec.prefabPath = generatePrefabPath(id, firstName, lastName); + rec.persistent = persistent; - if (!templatePath.empty() && - std::filesystem::exists(templatePath)) { - copyPrefab(templatePath, rec.prefabPath); + if (persistent) { + rec.prefabPath = generatePrefabPath(id, firstName, lastName); + if (!templatePath.empty() && + std::filesystem::exists(templatePath)) { + copyPrefab(templatePath, rec.prefabPath); + } } m_characters[id] = rec; - autoSave(); + if (persistent) + autoSave(); return id; } @@ -763,6 +1114,21 @@ nlohmann::json CharacterRegistry::serialize() const j["version"] = "1.0"; j["nextId"] = m_nextId; + for (const auto &name : m_maleFirstNameSeeds) + j["maleFirstNameSeeds"].push_back(name); + for (const auto &name : m_femaleFirstNameSeeds) + j["femaleFirstNameSeeds"].push_back(name); + for (const auto &name : m_lastNameSeeds) + j["lastNameSeeds"].push_back(name); + + for (const auto &key : m_birthRandomizableShapeKeys) + j["birthRandomizableShapeKeys"].push_back(key); + for (const auto &key : m_birthExcludedShapeKeys) + j["birthExcludedShapeKeys"].push_back(key); + + j["basePregnancyDuration"] = m_basePregnancyDuration; + j["pregnancyTimeScale"] = m_pregnancyTimeScale; + for (const auto &c : m_characterColumns) { nlohmann::json col; col["name"] = c.name; @@ -782,8 +1148,11 @@ nlohmann::json CharacterRegistry::serialize() const for (const auto &pair : m_characters) { const CharacterRecord &c = pair.second; + if (!c.persistent) + continue; nlohmann::json rec; rec["id"] = c.id; + rec["persistent"] = c.persistent; rec["firstName"] = c.firstName; rec["lastName"] = c.lastName; rec["prefabPath"] = c.prefabPath; @@ -792,6 +1161,27 @@ nlohmann::json CharacterRegistry::serialize() const rec["currentXP"] = c.currentXP; rec["availablePoints"] = c.availablePoints; rec["levelUpPending"] = c.levelUpPending; + rec["ageYears"] = c.ageYears; + rec["inlineAge"] = c.inlineAge; + rec["inlineSex"] = c.inlineSex; + rec["inlineOutfitLevel"] = c.inlineOutfitLevel; + if (!c.inlineSlotSelections.empty()) { + nlohmann::json selJson; + for (const auto &kv : c.inlineSlotSelections) { + nlohmann::json s; + s["layer1Mesh"] = kv.second.layer1Mesh; + s["layer2Mesh"] = kv.second.layer2Mesh; + s["explicitMesh"] = kv.second.explicitMesh; + selJson[kv.first] = s; + } + rec["inlineSlotSelections"] = selJson; + } + if (!c.inlineShapeKeyWeights.empty()) { + nlohmann::json skJson; + for (const auto &kv : c.inlineShapeKeyWeights) + skJson[kv.first] = kv.second; + rec["inlineShapeKeys"] = skJson; + } for (const auto &kv : c.stats) rec["stats"][kv.first] = kv.second; for (const auto &kv : c.skills) @@ -852,6 +1242,30 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) m_nextId = j.value("nextId", 1); + if (j.contains("maleFirstNameSeeds")) { + for (const auto &name : j["maleFirstNameSeeds"]) + m_maleFirstNameSeeds.push_back(name.get()); + } + if (j.contains("femaleFirstNameSeeds")) { + for (const auto &name : j["femaleFirstNameSeeds"]) + m_femaleFirstNameSeeds.push_back(name.get()); + } + if (j.contains("lastNameSeeds")) { + for (const auto &name : j["lastNameSeeds"]) + m_lastNameSeeds.push_back(name.get()); + } + if (j.contains("birthRandomizableShapeKeys")) { + for (const auto &key : j["birthRandomizableShapeKeys"]) + m_birthRandomizableShapeKeys.push_back(key.get()); + } + if (j.contains("birthExcludedShapeKeys")) { + for (const auto &key : j["birthExcludedShapeKeys"]) + m_birthExcludedShapeKeys.push_back(key.get()); + } + + m_basePregnancyDuration = j.value("basePregnancyDuration", 300.0f); + m_pregnancyTimeScale = j.value("pregnancyTimeScale", 1.0f); + if (j.contains("characterColumns")) { for (const auto &col : j["characterColumns"]) { ColumnDef::Type t = ColumnDef::String; @@ -880,14 +1294,36 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) for (const auto &rec : j["characters"]) { CharacterRecord c; c.id = rec.value("id", 0); + c.persistent = rec.value("persistent", true); c.firstName = rec.value("firstName", ""); c.lastName = rec.value("lastName", ""); + c.pregnantByFatherId = rec.value("pregnantByFatherId", 0); + c.pregnancyProgress = rec.value("pregnancyProgress", 0.0f); + c.pregnancyMaxProgress = rec.value("pregnancyMaxProgress", 0.0f); + c.basePregnancyDuration = rec.value("basePregnancyDuration", 0.0f); c.prefabPath = rec.value("prefabPath", ""); c.className = rec.value("className", ""); c.level = rec.value("level", 1); c.currentXP = rec.value("currentXP", 0); c.availablePoints = rec.value("availablePoints", 0); c.levelUpPending = rec.value("levelUpPending", false); + c.ageYears = rec.value("ageYears", 0); + c.inlineAge = rec.value("inlineAge", "adult"); + c.inlineSex = rec.value("inlineSex", "male"); + c.inlineOutfitLevel = rec.value("inlineOutfitLevel", 2); + if (rec.contains("inlineSlotSelections")) { + for (auto &[slot, s] : rec["inlineSlotSelections"].items()) { + SlotSelection sel; + sel.layer1Mesh = s.value("layer1Mesh", ""); + sel.layer2Mesh = s.value("layer2Mesh", ""); + sel.explicitMesh = s.value("explicitMesh", ""); + c.inlineSlotSelections[slot] = sel; + } + } + if (rec.contains("inlineShapeKeys")) { + for (auto &[k, v] : rec["inlineShapeKeys"].items()) + c.inlineShapeKeyWeights[k] = v.get(); + } if (rec.contains("stats")) { for (auto &[k, v] : rec["stats"].items()) c.stats[k] = v.get(); @@ -1098,9 +1534,9 @@ void CharacterRegistry::drawEditor(bool *p_open) } const char *tabs[] = {"Characters", "Groups", "Relationships", - "Columns"}; + "Names", "Columns"}; if (ImGui::BeginTabBar("RegistryTabs")) { - for (int i = 0; i < 4; ++i) { + for (int i = 0; i < 5; ++i) { if (ImGui::BeginTabItem(tabs[i])) { m_editorTab = i; ImGui::EndTabItem(); @@ -1112,11 +1548,15 @@ void CharacterRegistry::drawEditor(bool *p_open) switch (m_editorTab) { case 0: /* ---------- Characters ---------- */ + static bool addRuntime = false; if (ImGui::Button("Add Character")) { - uint64_t id = createCharacter("New", "Character"); + uint64_t id = createCharacter("New", "Character", "", + addRuntime); m_selectedCharacterId = id; } ImGui::SameLine(); + ImGui::Checkbox("Runtime", &addRuntime); + ImGui::SameLine(); if (ImGui::Button("Delete") && m_selectedCharacterId != 0) { deleteCharacter(m_selectedCharacterId); m_selectedCharacterId = 0; @@ -1135,7 +1575,7 @@ void CharacterRegistry::drawEditor(bool *p_open) 40); ImGui::TableSetupColumn("First Name"); ImGui::TableSetupColumn("Last Name"); - ImGui::TableSetupColumn("Age", + ImGui::TableSetupColumn("Age(Yrs)", ImGuiTableColumnFlags_WidthFixed, 50); ImGui::TableSetupColumn("Sex", @@ -1161,15 +1601,21 @@ void CharacterRegistry::drawEditor(bool *p_open) m_selectedCharacterId = c.id; ImGui::TableSetColumnIndex(1); - ImGui::Text("%s", c.firstName.c_str()); + ImGui::Text("%s%s", + c.persistent ? "" : "[R] ", + c.firstName.c_str()); ImGui::TableSetColumnIndex(2); ImGui::Text("%s", c.lastName.c_str()); std::string age = "?", sex = "?"; int outfit = 2; - readPrefabSlots(c.prefabPath, age, sex, outfit); + if (!readPrefabSlots(c.prefabPath, age, sex, outfit)) { + age = c.inlineAge; + sex = c.inlineSex; + outfit = c.inlineOutfitLevel; + } ImGui::TableSetColumnIndex(3); - ImGui::Text("%s", age.c_str()); + ImGui::Text("%d", c.ageYears); ImGui::TableSetColumnIndex(4); ImGui::Text("%s", sex.c_str()); @@ -1243,8 +1689,19 @@ void CharacterRegistry::drawEditor(bool *p_open) lastId = c->id; } ImGui::Separator(); - ImGui::Text("Character #%lu", - (unsigned long)c->id); + ImGui::Text("Character #%lu %s", + (unsigned long)c->id, + c->persistent ? "" : "[RUNTIME]"); + if (!c->persistent) { + ImGui::SameLine(); + if (ImGui::SmallButton("Promote to Roster")) { + c->persistent = true; + c->prefabPath = generatePrefabPath( + c->id, c->firstName, + c->lastName); + autoSave(); + } + } if (ImGui::InputText("First Name", fnBuf, sizeof(fnBuf))) { c->firstName = fnBuf; @@ -1258,8 +1715,19 @@ void CharacterRegistry::drawEditor(bool *p_open) /* Name generation */ ImGui::Separator(); - if (ImGui::Button("Generate First Name")) { - std::string g = generateFirstName(); + if (ImGui::Button("Generate Male First Name")) { + std::string g = generateFirstName("male"); + if (!g.empty()) { + snprintf(fnBuf, sizeof(fnBuf), "%s", + g.c_str()); + c->firstName = fnBuf; + autoSave(); + learnNamesFromRegistry(); + } + } + ImGui::SameLine(); + if (ImGui::Button("Generate Female First Name")) { + std::string g = generateFirstName("female"); if (!g.empty()) { snprintf(fnBuf, sizeof(fnBuf), "%s", g.c_str()); @@ -1283,27 +1751,141 @@ void CharacterRegistry::drawEditor(bool *p_open) if (ImGui::Button("Re-learn Names")) learnNamesFromRegistry(); - /* Preview samples */ - if (!m_firstNameGen.empty()) { - ImGui::TextDisabled("First name samples:"); - auto samples = m_firstNameGen.generateMany( - 5, 3, 12, nullptr); - for (size_t i = 0; i < samples.size(); ++i) { - if (i > 0) - ImGui::SameLine(); - ImGui::TextDisabled("%s", - samples[i].c_str()); - } + /* Age */ + int ageYrs = c->ageYears; + if (ImGui::InputInt("Age (years)", &ageYrs)) { + c->ageYears = ageYrs; + autoSave(); } - if (!m_lastNameGen.empty()) { - ImGui::TextDisabled("Last name samples:"); - auto samples = m_lastNameGen.generateMany( - 5, 3, 12, nullptr); - for (size_t i = 0; i < samples.size(); ++i) { - if (i > 0) - ImGui::SameLine(); - ImGui::TextDisabled("%s", - samples[i].c_str()); + + /* Family */ + ImGui::Separator(); + ImGui::Text("Family"); + auto parents = getParents(c->id); + if (!parents.empty()) { + ImGui::Text("Parents:"); + for (uint64_t pid : parents) { + const CharacterRecord *p = findCharacter(pid); + if (!p) + continue; + char lbl[256]; + snprintf(lbl, sizeof(lbl), "#%lu %s %s", + (unsigned long)pid, + p->firstName.c_str(), + p->lastName.c_str()); + if (ImGui::SmallButton(lbl)) + m_selectedCharacterId = pid; + ImGui::SameLine(); + } + ImGui::NewLine(); + } + auto children = getChildren(c->id); + if (!children.empty()) { + ImGui::Text("Children:"); + for (uint64_t cid : children) { + const CharacterRecord *ch = findCharacter(cid); + if (!ch) + continue; + char lbl[256]; + snprintf(lbl, sizeof(lbl), "#%lu %s %s", + (unsigned long)cid, + ch->firstName.c_str(), + ch->lastName.c_str()); + if (ImGui::SmallButton(lbl)) + m_selectedCharacterId = cid; + ImGui::SameLine(); + } + ImGui::NewLine(); + } + /* Two-parent birth */ + static uint64_t partnerId = 0; + char partnerLbl[256] = "(select partner)"; + if (partnerId != 0) { + const CharacterRecord *pc = findCharacter(partnerId); + if (pc) + labelBuf(partnerLbl, sizeof(partnerLbl), pc); + else + partnerId = 0; + } + if (ImGui::BeginCombo("Partner", partnerLbl)) { + for (const auto &pair : m_characters) { + if (pair.second.id == c->id) + continue; + char lbl[256]; + labelBuf(lbl, sizeof(lbl), &pair.second); + if (ImGui::Selectable(lbl, + partnerId == pair.second.id)) + partnerId = pair.second.id; + } + ImGui::EndCombo(); + } + if (partnerId != 0 && + ImGui::Button("Create Child")) { + uint64_t childId = createChild(c->id, partnerId); + if (childId != 0) + m_selectedCharacterId = childId; + } + + /* Pregnancy */ + ImGui::Separator(); + ImGui::Text("Pregnancy"); + if (c->pregnantByFatherId != 0) { + const CharacterRecord *father = findCharacter( + c->pregnantByFatherId); + ImGui::Text("Pregnant by: %s", + father ? (father->firstName + " " + + father->lastName).c_str() : + "unknown"); + float pct = 0.0f; + if (c->pregnancyMaxProgress > 0.0f) + pct = c->pregnancyProgress / + c->pregnancyMaxProgress; + if (pct > 1.0f) + pct = 1.0f; + ImGui::ProgressBar(pct, + ImVec2(0.0f, 0.0f)); + ImGui::Text("%.1f / %.1f s", + c->pregnancyProgress, + c->pregnancyMaxProgress); + if (ImGui::Button("Abort")) { + abortPregnancy(c->id); + } + float prog = c->pregnancyProgress; + if (ImGui::SliderFloat("Progress", &prog, + 0.0f, + c->pregnancyMaxProgress)) { + c->pregnancyProgress = prog; + autoSave(); + } + } else { + static uint64_t fatherId = 0; + char fatherLbl[256] = "(select father)"; + if (fatherId != 0) { + const CharacterRecord *fc = findCharacter(fatherId); + if (fc) + labelBuf(fatherLbl, + sizeof(fatherLbl), fc); + else + fatherId = 0; + } + if (ImGui::BeginCombo("Father", fatherLbl)) { + for (const auto &pair : m_characters) { + if (pair.second.id == c->id) + continue; + char lbl[256]; + labelBuf(lbl, sizeof(lbl), + &pair.second); + if (ImGui::Selectable( + lbl, + fatherId == pair.second.id)) + fatherId = pair.second.id; + } + ImGui::EndCombo(); + } + if (fatherId != 0 && + ImGui::Button("Make Pregnant")) { + conceive(c->id, fatherId); + fatherId = 0; } } @@ -1491,6 +2073,7 @@ void CharacterRegistry::drawEditor(bool *p_open) tagBuf[0] = '\0'; autoSave(); } + } } break; @@ -1841,7 +2424,202 @@ void CharacterRegistry::drawEditor(bool *p_open) } break; - case 3: + case 3: { + /* ---------- Names ---------- */ + if (ImGui::Button("Re-learn All Names")) + rebuildNameGenerators(); + ImGui::Separator(); + + /* Preview samples - cached to avoid flickering */ + { + static float lastSampleTime = -1.0f; + static std::vector maleSamples; + static std::vector femaleSamples; + static std::vector lastSamples; + if (ImGui::GetTime() - lastSampleTime > 1.0f) { + lastSampleTime = ImGui::GetTime(); + maleSamples = m_maleFirstNameGen.generateMany( + 5, 3, 12, nullptr); + femaleSamples = m_femaleFirstNameGen.generateMany( + 5, 3, 12, nullptr); + lastSamples = m_lastNameGen.generateMany( + 5, 3, 12, nullptr); + } + if (!m_maleFirstNameGen.empty()) { + ImGui::TextDisabled("Male first name samples:"); + for (size_t i = 0; i < maleSamples.size(); ++i) { + if (i > 0) + ImGui::SameLine(); + ImGui::TextDisabled("%s", + maleSamples[i].c_str()); + } + } + if (!m_femaleFirstNameGen.empty()) { + ImGui::TextDisabled("Female first name samples:"); + for (size_t i = 0; i < femaleSamples.size(); ++i) { + if (i > 0) + ImGui::SameLine(); + ImGui::TextDisabled("%s", + femaleSamples[i].c_str()); + } + } + if (!m_lastNameGen.empty()) { + ImGui::TextDisabled("Last name samples:"); + for (size_t i = 0; i < lastSamples.size(); ++i) { + if (i > 0) + ImGui::SameLine(); + ImGui::TextDisabled("%s", + lastSamples[i].c_str()); + } + } + } + + /* Seed name lists with stored seeds */ + auto drawSeedList = [&](const char *label, + std::vector &seeds) { + ImGui::Text("%s (%zu):", label, seeds.size()); + for (size_t i = 0; i < seeds.size();) { + ImGui::PushID(static_cast(i + 5000)); + ImGui::Text("%s", seeds[i].c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("x")) { + seeds.erase(seeds.begin() + i); + autoSave(); + } else { + ++i; + } + ImGui::PopID(); + } + }; + + drawSeedList("Male first name seeds", + m_maleFirstNameSeeds); + drawSeedList("Female first name seeds", + m_femaleFirstNameSeeds); + drawSeedList("Last name seeds", m_lastNameSeeds); + + /* Bulk add seeds */ + static char maleSeedBuf[4096] = ""; + static char femaleSeedBuf[4096] = ""; + static char lastSeedBuf[4096] = ""; + ImGui::InputTextMultiline("Male first names", + maleSeedBuf, + sizeof(maleSeedBuf), + ImVec2(0, 60)); + ImGui::InputTextMultiline("Female first names", + femaleSeedBuf, + sizeof(femaleSeedBuf), + ImVec2(0, 60)); + ImGui::InputTextMultiline("Last names", + lastSeedBuf, + sizeof(lastSeedBuf), + ImVec2(0, 60)); + if (ImGui::Button("Add Seeds")) { + auto parseLines = [](const char *buf) { + std::vector lines; + const char *p = buf; + while (*p) { + const char *end = p; + while (*end && *end != '\n' && + *end != '\r') + ++end; + std::string line(p, end - p); + size_t start = line.find_first_not_of(" \t"); + if (start != std::string::npos) { + size_t last = line.find_last_not_of(" \t"); + lines.push_back(line.substr(start, + last - start + 1)); + } + p = end; + while (*p == '\n' || *p == '\r') + ++p; + } + return lines; + }; + for (const auto &name : parseLines(maleSeedBuf)) + m_maleFirstNameSeeds.push_back(name); + for (const auto &name : parseLines(femaleSeedBuf)) + m_femaleFirstNameSeeds.push_back(name); + for (const auto &name : parseLines(lastSeedBuf)) + m_lastNameSeeds.push_back(name); + maleSeedBuf[0] = '\0'; + femaleSeedBuf[0] = '\0'; + lastSeedBuf[0] = '\0'; + rebuildNameGenerators(); + autoSave(); + } + + /* Pregnancy global config */ + ImGui::Separator(); + ImGui::Text("Pregnancy"); + float baseDur = m_basePregnancyDuration; + if (ImGui::InputFloat("Base Duration (s)", &baseDur)) { + m_basePregnancyDuration = baseDur; + autoSave(); + } + float ts = m_pregnancyTimeScale; + if (ImGui::InputFloat("Time Scale", &ts)) { + m_pregnancyTimeScale = ts; + autoSave(); + } + + /* Birth configuration */ + ImGui::Separator(); + ImGui::Text("Birth Config"); + ImGui::TextDisabled("Shape keys randomized at birth:"); + for (size_t i = 0; + i < m_birthRandomizableShapeKeys.size();) { + ImGui::PushID(static_cast(i)); + ImGui::Text("%s", + m_birthRandomizableShapeKeys[i].c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("x")) { + m_birthRandomizableShapeKeys.erase( + m_birthRandomizableShapeKeys.begin() + i); + autoSave(); + } else { + ++i; + } + ImGui::PopID(); + } + static char rkBuf[64] = ""; + ImGui::InputText("Add key", rkBuf, sizeof(rkBuf)); + ImGui::SameLine(); + if (ImGui::SmallButton("Add") && rkBuf[0] != '\0') { + m_birthRandomizableShapeKeys.push_back(rkBuf); + rkBuf[0] = '\0'; + autoSave(); + } + + ImGui::TextDisabled( + "Excluded keys (never copied or randomized):"); + for (size_t i = 0; + i < m_birthExcludedShapeKeys.size();) { + ImGui::PushID(static_cast(i + 1000)); + ImGui::Text("%s", + m_birthExcludedShapeKeys[i].c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("x")) { + m_birthExcludedShapeKeys.erase( + m_birthExcludedShapeKeys.begin() + i); + autoSave(); + } else { + ++i; + } + ImGui::PopID(); + } + static char ekBuf[64] = ""; + ImGui::InputText("Add excluded", ekBuf, sizeof(ekBuf)); + ImGui::SameLine(); + if (ImGui::SmallButton("Add") && ekBuf[0] != '\0') { + m_birthExcludedShapeKeys.push_back(ekBuf); + ekBuf[0] = '\0'; + autoSave(); + } + break; + } + + case 4: /* ---------- Columns ---------- */ drawColumnsEditor("Character Columns", m_characterColumns, *this); ImGui::Separator(); diff --git a/src/features/editScene/systems/CharacterRegistry.hpp b/src/features/editScene/systems/CharacterRegistry.hpp index 1e3e620..a904ca7 100644 --- a/src/features/editScene/systems/CharacterRegistry.hpp +++ b/src/features/editScene/systems/CharacterRegistry.hpp @@ -12,6 +12,7 @@ #include #include "MarkovNameGenerator.hpp" +#include "../components/CharacterSlots.hpp" class EditorUISystem; @@ -63,6 +64,25 @@ public: std::unordered_map currentPools; bool levelUpPending = false; + /* Age in years (runtime progression) */ + int ageYears = 0; + + /* Inline appearance (used when no prefab file exists) */ + std::string inlineAge = "adult"; + std::string inlineSex = "male"; + int inlineOutfitLevel = 2; + std::unordered_map inlineSlotSelections; + std::unordered_map inlineShapeKeyWeights; + + /* Runtime-only characters are NOT saved to character_registry.json */ + bool persistent = true; + + /* Pregnancy state (progresses whether spawned or not) */ + uint64_t pregnantByFatherId = 0; + float pregnancyProgress = 0.0f; + float pregnancyMaxProgress = 0.0f; + float basePregnancyDuration = 0.0f; /* 0 = use global */ + /* Tags */ std::vector tags; @@ -123,7 +143,8 @@ public: /* ------------------------------------------------------------------ */ uint64_t createCharacter(const std::string &firstName, const std::string &lastName, - const std::string &templatePath = ""); + const std::string &templatePath = "", + bool persistent = true); void deleteCharacter(uint64_t id); CharacterRecord *findCharacter(uint64_t id); const CharacterRecord *findCharacter(uint64_t id) const; @@ -221,14 +242,75 @@ public: /* Name Generation */ /* ------------------------------------------------------------------ */ void learnNamesFromRegistry(); - std::string generateFirstName() const; + std::string generateFirstName(const std::string &sex = "male") const; std::string generateLastName() const; + const MarkovNameGenerator &getMaleFirstNameGen() const + { + return m_maleFirstNameGen; + } + const MarkovNameGenerator &getFemaleFirstNameGen() const + { + return m_femaleFirstNameGen; + } + + std::vector &getMaleFirstNameSeeds() + { + return m_maleFirstNameSeeds; + } + std::vector &getFemaleFirstNameSeeds() + { + return m_femaleFirstNameSeeds; + } + std::vector &getLastNameSeeds() + { + return m_lastNameSeeds; + } + + /* Family / Birth */ + /* ------------------------------------------------------------------ */ + std::vector getParents(uint64_t childId) const; + std::vector getChildren(uint64_t parentId) const; + uint64_t createChild(uint64_t parentA, uint64_t parentB); + + /* Pregnancy helpers */ + bool conceive(uint64_t motherId, uint64_t fatherId); + void abortPregnancy(uint64_t motherId); + bool isPregnant(uint64_t motherId) const; + + float getBasePregnancyDuration() const + { + return m_basePregnancyDuration; + } + void setBasePregnancyDuration(float v) + { + m_basePregnancyDuration = v; + } + float getPregnancyTimeScale() const + { + return m_pregnancyTimeScale; + } + void setPregnancyTimeScale(float v) + { + m_pregnancyTimeScale = v; + } + + std::vector &getBirthRandomizableShapeKeys() + { + return m_birthRandomizableShapeKeys; + } + std::vector &getBirthExcludedShapeKeys() + { + return m_birthExcludedShapeKeys; + } + /* Spawn / Save */ /* ------------------------------------------------------------------ */ flecs::entity findSpawnedEntity(uint64_t id) const; bool despawnCharacter(uint64_t id); flecs::entity spawnCharacter(uint64_t id); + flecs::entity spawnInlineCharacter(const CharacterRecord &c, + const Ogre::Vector3 &pos); bool savePrefabForCharacter(uint64_t id); /** @@ -258,6 +340,7 @@ public: private: uint64_t m_nextId = 1; + uint64_t m_nextRuntimeId = 1ull << 32; std::unordered_map m_characters; std::unordered_map m_groups; @@ -274,9 +357,23 @@ private: std::string m_autoSavePath; std::vector m_templates; - MarkovNameGenerator m_firstNameGen; + MarkovNameGenerator m_maleFirstNameGen; + MarkovNameGenerator m_femaleFirstNameGen; MarkovNameGenerator m_lastNameGen; + std::vector m_maleFirstNameSeeds; + std::vector m_femaleFirstNameSeeds; + std::vector m_lastNameSeeds; + + std::vector m_birthRandomizableShapeKeys; + std::vector m_birthExcludedShapeKeys; + + /* Global pregnancy config */ + float m_basePregnancyDuration = 300.0f; + float m_pregnancyTimeScale = 1.0f; + + void rebuildNameGenerators(); + flecs::world *m_world = nullptr; Ogre::SceneManager *m_sceneMgr = nullptr; EditorUISystem *m_uiSystem = nullptr; diff --git a/src/features/editScene/systems/PregnancySystem.cpp b/src/features/editScene/systems/PregnancySystem.cpp new file mode 100644 index 0000000..2ef304d --- /dev/null +++ b/src/features/editScene/systems/PregnancySystem.cpp @@ -0,0 +1,79 @@ +#include "PregnancySystem.hpp" +#include "CharacterRegistry.hpp" +#include "EventBus.hpp" +#include "../components/EventParams.hpp" +#include + +PregnancySystem::PregnancySystem(flecs::world &world) + : m_world(world) +{ +} + +PregnancySystem::~PregnancySystem() = default; + +void PregnancySystem::update(float deltaTime) +{ + advancePregnancies(deltaTime); + checkBirths(); +} + +void PregnancySystem::advancePregnancies(float dt) +{ + CharacterRegistry ® = CharacterRegistry::getSingleton(); + float timeScale = reg.getPregnancyTimeScale(); + if (timeScale <= 0.0f) + return; + + for (const auto &pair : reg.getCharacters()) { + uint64_t id = pair.first; + if (pair.second.pregnantByFatherId == 0) + continue; + CharacterRegistry::CharacterRecord *c = + reg.findCharacter(id); + if (c) + c->pregnancyProgress += dt * timeScale; + } +} + +void PregnancySystem::checkBirths() +{ + CharacterRegistry ® = CharacterRegistry::getSingleton(); + std::vector toBirth; + + for (const auto &pair : reg.getCharacters()) { + const CharacterRegistry::CharacterRecord &c = pair.second; + if (c.pregnantByFatherId == 0) + continue; + if (c.pregnancyMaxProgress > 0.0f && + c.pregnancyProgress >= c.pregnancyMaxProgress) + toBirth.push_back(c.id); + } + + for (uint64_t motherId : toBirth) { + CharacterRegistry::CharacterRecord *mother = + reg.findCharacter(motherId); + if (!mother) + continue; + uint64_t fatherId = mother->pregnantByFatherId; + + uint64_t childId = reg.createChild(fatherId, motherId); + + /* Fire birth event */ + editScene::EventParams params; + params.setEntityId("mother", motherId); + params.setEntityId("father", fatherId); + params.setEntityId("child", childId); + EventBus::getInstance().send("birth", params); + + /* Reset pregnancy */ + mother->pregnantByFatherId = 0; + mother->pregnancyProgress = 0.0f; + mother->pregnancyMaxProgress = 0.0f; + + Ogre::LogManager::getSingleton().logMessage( + "PregnancySystem: birth — mother=" + + Ogre::StringConverter::toString(motherId) + + " father=" + Ogre::StringConverter::toString(fatherId) + + " child=" + Ogre::StringConverter::toString(childId)); + } +} diff --git a/src/features/editScene/systems/PregnancySystem.hpp b/src/features/editScene/systems/PregnancySystem.hpp new file mode 100644 index 0000000..19162ee --- /dev/null +++ b/src/features/editScene/systems/PregnancySystem.hpp @@ -0,0 +1,28 @@ +#ifndef EDITSCENE_PREGNANCY_SYSTEM_HPP +#define EDITSCENE_PREGNANCY_SYSTEM_HPP +#pragma once + +#include + +class CharacterRegistry; + +/** + * System that advances pregnancy progress for all female characters + * in the CharacterRegistry and triggers birth when progression completes. + */ +class PregnancySystem { +public: + PregnancySystem(flecs::world &world); + ~PregnancySystem(); + + /** Call every frame. Advances pregnancies and triggers births. */ + void update(float deltaTime); + +private: + void advancePregnancies(float dt); + void checkBirths(); + + flecs::world &m_world; +}; + +#endif // EDITSCENE_PREGNANCY_SYSTEM_HPP diff --git a/src/features/editScene/tests/character_lua_test.cpp b/src/features/editScene/tests/character_lua_test.cpp new file mode 100644 index 0000000..b1ad641 --- /dev/null +++ b/src/features/editScene/tests/character_lua_test.cpp @@ -0,0 +1,526 @@ +/** + * @file character_lua_test.cpp + * @brief Standalone test for the Lua Character API. + * + * Tests runtime character creation, pregnancy, birth, lineage, + * spawn/despawn, and birth-event tracking via the ecs.character.* + * Lua API. + * + * Examples included in test comments show how to use each API + * from Lua game scripts. + * + * Build with: + * g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \ + * character_lua_test.cpp \ + * ../lua/LuaEntityApi.cpp \ + * ../lua/LuaEventApi.cpp \ + * ../lua/LuaCharacterApi.cpp \ + * ../../lua/lua-5.4.8/src/liblua.a \ + * -o character_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 +} + +// Flecs stub for standalone testing +namespace flecs +{ + +struct entity { + uint64_t m_id = 0; + bool m_valid = false; + + entity() + : m_id(0) + , m_valid(false) + { + } + explicit entity(uint64_t id) + : m_id(id) + , m_valid(true) + { + } + + uint64_t id() const + { + return m_id; + } + bool is_valid() const + { + return m_valid; + } + bool is_alive() const + { + return m_valid; + } + const char *name() const + { + return ""; + } + void set_name(const char *) + { + } + void destruct() + { + m_valid = false; + } + + entity parent() const + { + return entity(); + } + + template void children(Func) const + { + } + + template void add() + { + } + + template bool has() const + { + return false; + } + + template const T *get() const + { + return nullptr; + } + + template void set(const T &) + { + } + + bool operator==(const entity &other) const + { + return m_id == other.m_id; + } +}; + +using entity_t = uint64_t; + +struct world { + entity make_entity() + { + return entity(nextId++); + } + entity lookup(const char *) + { + return entity(); + } + + static world &get() + { + static world w; + return w; + } + +private: + uint64_t nextId = 1000; +}; + +} // namespace flecs + +// Forward declare registration functions +namespace editScene +{ +void registerLuaEntityApi(lua_State *L); +void registerLuaEventApi(lua_State *L); +void registerLuaCharacterApi(lua_State *L); +void clearStubCharacters(); +} + +// --------------------------------------------------------------------------- +// 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) + +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: Create persistent and runtime characters +// --------------------------------------------------------------------------- + +static int testCreateCharacters(lua_State *L) +{ + TEST("create persistent and runtime characters"); + + /* + * Example: Create a persistent roster character + * local id = ecs.character.create("Alice", "Smith", "", true) + * + * Example: Create a runtime-only (temporary) character + * local id = ecs.character.create("Bandit", "", "", false) + */ + + bool ok = runLua( + L, + "local p = ecs.character.create('Alice', 'Smith', '', true);" + "assert(type(p) == 'number', 'persistent id should be number');" + "assert(p > 0, 'persistent id should be positive');" + "local r = ecs.character.create('Bandit', '', '', false);" + "assert(type(r) == 'number', 'runtime id should be number');" + "assert(r > p, 'runtime id should be larger than persistent');" + "local c = ecs.character.find(p);" + "assert(c ~= nil, 'should find persistent char');" + "assert(c.firstName == 'Alice', 'name mismatch');" + "assert(c.persistent == true, 'should be persistent');" + "local c2 = ecs.character.find(r);" + "assert(c2.persistent == false, 'should be runtime');"); + if (!ok) + FAIL("create characters assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 2: Character listing and name changes +// --------------------------------------------------------------------------- + +static int testCharacterListingAndNames(lua_State *L) +{ + TEST("list characters and change names"); + + /* + * Example: List all characters + * local all = ecs.character.get_all() + * for i, id in ipairs(all) do ... end + * + * Example: Rename a character + * ecs.character.set_name(id, "NewFirst", "NewLast") + */ + + bool ok = runLua( + L, + "ecs.character.create('Bob', 'Jones', '', true);" + "ecs.character.create('Carol', 'Dane', '', true);" + "local all = ecs.character.get_all();" + "assert(#all >= 2, 'should have at least 2 chars');" + "local first = all[1];" + "ecs.character.set_name(first, 'Robert', 'Jones-Junior');" + "local c = ecs.character.find(first);" + "assert(c.firstName == 'Robert', 'first name not updated');" + "assert(c.lastName == 'Jones-Junior', 'last name not updated');"); + if (!ok) + FAIL("listing/names assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 3: Spawn and despawn +// --------------------------------------------------------------------------- + +static int testSpawnDespawn(lua_State *L) +{ + TEST("spawn and despawn characters"); + + /* + * Example: Spawn a character into the world + * local entityId = ecs.character.spawn(charId) + * if entityId ~= nil then ... end + * + * Example: Check if spawned + * if ecs.character.is_spawned(charId) then ... end + * + * Example: Despawn + * ecs.character.despawn(charId) + */ + + bool ok = runLua( + L, + "local id = ecs.character.create('Dave', 'Miller', '', true);" + "assert(ecs.character.is_spawned(id) == false, 'should not be spawned');" + "local eid = ecs.character.spawn(id);" + "assert(eid ~= nil, 'spawn should return entity id');" + "assert(type(eid) == 'number', 'entity id should be number');" + "assert(ecs.character.is_spawned(id) == true, 'should be spawned');" + "local ok = ecs.character.despawn(id);" + "assert(ok == true, 'despawn should succeed');" + "assert(ecs.character.is_spawned(id) == false, 'should not be spawned after despawn');"); + if (!ok) + FAIL("spawn/despawn assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 4: Conceive and pregnancy state +// --------------------------------------------------------------------------- + +static int testConceiveAndPregnancy(lua_State *L) +{ + TEST("conceive and query pregnancy state"); + + /* + * Example: Make a character pregnant + * local ok = ecs.character.conceive(motherId, fatherId) + * if ok then ... end + * + * Example: Check pregnancy + * if ecs.character.is_pregnant(motherId) then ... end + * + * Example: Get progress + * local prog = ecs.character.get_pregnancy_progress(motherId) + * print(prog.progress .. " / " .. prog.maxProgress) + */ + + bool ok = runLua( + L, + "local mother = ecs.character.create('Eve', 'Adams', '', true);" + "local father = ecs.character.create('Adam', 'Adams', '', true);" + "assert(ecs.character.is_pregnant(mother) == false, 'should not be pregnant initially');" + "local ok = ecs.character.conceive(mother, father);" + "assert(ok == true, 'conceive should succeed');" + "assert(ecs.character.is_pregnant(mother) == true, 'should be pregnant');" + "local prog = ecs.character.get_pregnancy_progress(mother);" + "assert(prog ~= nil, 'progress should not be nil');" + "assert(prog.progress == 0, 'initial progress should be 0');" + "assert(prog.maxProgress > 0, 'maxProgress should be positive');" + "assert(prog.ratio == 0, 'initial ratio should be 0');"); + if (!ok) + FAIL("conceive/pregnancy assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 5: Abort pregnancy +// --------------------------------------------------------------------------- + +static int testAbortPregnancy(lua_State *L) +{ + TEST("abort pregnancy"); + + /* + * Example: Abort a pregnancy + * ecs.character.abort_pregnancy(motherId) + */ + + bool ok = runLua( + L, + "local mother = ecs.character.create('Fay', 'Wray', '', true);" + "local father = ecs.character.create('King', 'Kong', '', true);" + "ecs.character.conceive(mother, father);" + "assert(ecs.character.is_pregnant(mother) == true);" + "ecs.character.abort_pregnancy(mother);" + "assert(ecs.character.is_pregnant(mother) == false, 'should not be pregnant after abort');" + "local prog = ecs.character.get_pregnancy_progress(mother);" + "assert(prog == nil, 'progress should be nil after abort');"); + if (!ok) + FAIL("abort pregnancy assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 6: Create child and lineage +// --------------------------------------------------------------------------- + +static int testCreateChildAndLineage(lua_State *L) +{ + TEST("create child and query lineage"); + + /* + * Example: Create a child from two parents + * local childId = ecs.character.create_child(fatherId, motherId) + * + * Example: Get parents of a child + * local parents = ecs.character.get_parents(childId) + * + * Example: Get children of a parent + * local kids = ecs.character.get_children(parentId) + */ + + bool ok = runLua( + L, + "local dad = ecs.character.create('Homer', 'Simpson', '', true);" + "local mom = ecs.character.create('Marge', 'Simpson', '', true);" + "local child = ecs.character.create_child(dad, mom);" + "assert(type(child) == 'number', 'child id should be number');" + "local parents = ecs.character.get_parents(child);" + "assert(#parents == 2, 'should have 2 parents');" + "local dadKids = ecs.character.get_children(dad);" + "assert(#dadKids == 1, 'dad should have 1 child');" + "assert(dadKids[1] == child, 'dad child should match');" + "local momKids = ecs.character.get_children(mom);" + "assert(#momKids == 1, 'mom should have 1 child');" + "assert(momKids[1] == child, 'mom child should match');"); + if (!ok) + FAIL("child/lineage assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 7: Birth event tracking via EventBus +// --------------------------------------------------------------------------- + +static int testBirthEvent(lua_State *L) +{ + TEST("subscribe to birth event and receive it"); + + /* + * Example: Listen for birth events + * ecs.subscribe_event('birth', function(event, params) + * print('Birth! Mother=' .. params.mother) + * print('Father=' .. params.father) + * print('Child=' .. params.child) + * end) + * + * Example: Manually send a birth event (for scripted births) + * ecs.send_event('birth', { mother = momId, father = dadId, child = babyId }) + */ + + bool ok = runLua( + L, + "local birthData = nil;" + "local sub = ecs.subscribe_event('birth', function(event, params)\n" + " birthData = params;\n" + "end);" + "local mom = ecs.character.create('Sarah', 'Connor', '', true);" + "local dad = ecs.character.create('Kyle', 'Reese', '', true);" + "local baby = ecs.character.create_child(dad, mom);" + "ecs.send_event('birth', { mother = mom, father = dad, child = baby });" + "assert(birthData ~= nil, 'birth event should have been received');" + "assert(birthData.mother == mom, 'mother id mismatch');" + "assert(birthData.father == dad, 'father id mismatch');" + "assert(birthData.child == baby, 'child id mismatch');"); + if (!ok) + FAIL("birth event assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 8: Delete character +// --------------------------------------------------------------------------- + +static int testDeleteCharacter(lua_State *L) +{ + TEST("delete character"); + + /* + * Example: Delete a character permanently + * ecs.character.delete(charId) + */ + + bool ok = runLua( + L, + "local id = ecs.character.create('Goner', 'Dead', '', true);" + "assert(ecs.character.find(id) ~= nil, 'should exist');" + "ecs.character.delete(id);" + "assert(ecs.character.find(id) == nil, 'should not exist after delete');"); + if (!ok) + FAIL("delete character assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +int main() +{ + printf("Character Lua API Tests\n"); + printf("========================\n\n"); + + lua_State *L = luaL_newstate(); + if (!L) { + fprintf(stderr, "Failed to create Lua state\n"); + return 1; + } + luaL_openlibs(L); + + // Register APIs + editScene::registerLuaEntityApi(L); + editScene::registerLuaEventApi(L); + editScene::registerLuaCharacterApi(L); + + // Run tests (clear stub state between each for isolation) + int failures = 0; + + editScene::clearStubCharacters(); + failures += testCreateCharacters(L); + + editScene::clearStubCharacters(); + failures += testCharacterListingAndNames(L); + + editScene::clearStubCharacters(); + failures += testSpawnDespawn(L); + + editScene::clearStubCharacters(); + failures += testConceiveAndPregnancy(L); + + editScene::clearStubCharacters(); + failures += testAbortPregnancy(L); + + editScene::clearStubCharacters(); + failures += testCreateChildAndLineage(L); + + editScene::clearStubCharacters(); + failures += testBirthEvent(L); + + editScene::clearStubCharacters(); + failures += testDeleteCharacter(L); + + 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 7785b18..f2f92ba 100644 --- a/src/features/editScene/tests/lua_test_stubs.cpp +++ b/src/features/editScene/tests/lua_test_stubs.cpp @@ -224,6 +224,324 @@ void registerLuaDialogueApi(lua_State *L) } // namespace editScene +// --------------------------------------------------------------------------- +// Stub: LuaCharacterApi +// --------------------------------------------------------------------------- + +namespace editScene +{ + +struct StubCharacter { + uint64_t id = 0; + std::string firstName; + std::string lastName; + std::string sex = "male"; + bool persistent = true; + uint64_t pregnantByFatherId = 0; + float pregnancyProgress = 0.0f; + float pregnancyMaxProgress = 0.0f; + bool spawned = false; + int entityId = -1; +}; + +static std::unordered_map s_stubCharacters; +static std::unordered_map > s_stubParents; +static std::unordered_map > s_stubChildren; +static uint64_t s_stubNextCharId = 1; +static int s_stubNextEntityId = 5000; + +void clearStubCharacters() +{ + s_stubCharacters.clear(); + s_stubParents.clear(); + s_stubChildren.clear(); + s_stubNextCharId = 1; + s_stubNextEntityId = 5000; +} + +void registerLuaCharacterApi(lua_State *L) +{ + lua_getglobal(L, "ecs"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + + lua_newtable(L); // ecs.character + + // create(firstName, lastName, templatePath, persistent) + lua_pushcfunction(L, [](lua_State *L) -> int { + const char *fn = lua_tostring(L, 1); + const char *ln = lua_tostring(L, 2); + bool persistent = true; + if (lua_gettop(L) >= 4) + persistent = lua_toboolean(L, 4) != 0; + uint64_t id = s_stubNextCharId++; + StubCharacter c; + c.id = id; + c.firstName = fn ? fn : ""; + c.lastName = ln ? ln : ""; + c.persistent = persistent; + s_stubCharacters[id] = c; + lua_pushinteger(L, static_cast(id)); + return 1; + }); + lua_setfield(L, -2, "create"); + + // delete(id) + lua_pushcfunction(L, [](lua_State *L) -> int { + uint64_t id = static_cast(lua_tointeger(L, 1)); + s_stubCharacters.erase(id); + return 0; + }); + lua_setfield(L, -2, "delete"); + + // find(id) + lua_pushcfunction(L, [](lua_State *L) -> int { + uint64_t id = static_cast(lua_tointeger(L, 1)); + auto it = s_stubCharacters.find(id); + if (it == s_stubCharacters.end()) { + lua_pushnil(L); + return 1; + } + lua_newtable(L); + lua_pushinteger(L, static_cast(it->second.id)); + lua_setfield(L, -2, "id"); + lua_pushstring(L, it->second.firstName.c_str()); + lua_setfield(L, -2, "firstName"); + lua_pushstring(L, it->second.lastName.c_str()); + lua_setfield(L, -2, "lastName"); + lua_pushstring(L, it->second.sex.c_str()); + 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_setfield(L, -2, "pregnantByFatherId"); + lua_pushnumber(L, it->second.pregnancyProgress); + lua_setfield(L, -2, "pregnancyProgress"); + lua_pushnumber(L, it->second.pregnancyMaxProgress); + lua_setfield(L, -2, "pregnancyMaxProgress"); + return 1; + }); + lua_setfield(L, -2, "find"); + + // get_all() + lua_pushcfunction(L, [](lua_State *L) -> int { + lua_newtable(L); + int idx = 1; + for (auto &pair : s_stubCharacters) { + lua_pushinteger(L, + static_cast(pair.first)); + lua_rawseti(L, -2, idx++); + } + return 1; + }); + lua_setfield(L, -2, "get_all"); + + // set_name(id, firstName, lastName) + lua_pushcfunction(L, [](lua_State *L) -> int { + uint64_t id = static_cast(lua_tointeger(L, 1)); + auto it = s_stubCharacters.find(id); + if (it != s_stubCharacters.end()) { + const char *fn = lua_tostring(L, 2); + const char *ln = lua_tostring(L, 3); + if (fn) + it->second.firstName = fn; + if (ln) + it->second.lastName = ln; + } + return 0; + }); + lua_setfield(L, -2, "set_name"); + + // spawn(id) + lua_pushcfunction(L, [](lua_State *L) -> int { + uint64_t id = static_cast(lua_tointeger(L, 1)); + auto it = s_stubCharacters.find(id); + if (it == s_stubCharacters.end()) { + lua_pushnil(L); + return 1; + } + it->second.spawned = true; + it->second.entityId = s_stubNextEntityId++; + lua_pushinteger(L, it->second.entityId); + return 1; + }); + lua_setfield(L, -2, "spawn"); + + // despawn(id) + lua_pushcfunction(L, [](lua_State *L) -> int { + uint64_t id = static_cast(lua_tointeger(L, 1)); + auto it = s_stubCharacters.find(id); + if (it != s_stubCharacters.end()) { + it->second.spawned = false; + lua_pushboolean(L, 1); + } else { + lua_pushboolean(L, 0); + } + return 1; + }); + lua_setfield(L, -2, "despawn"); + + // is_spawned(id) + 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); + 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)); + auto it = s_stubCharacters.find(motherId); + if (it == s_stubCharacters.end()) { + lua_pushboolean(L, 0); + return 1; + } + it->second.pregnantByFatherId = fatherId; + it->second.pregnancyProgress = 0.0f; + it->second.pregnancyMaxProgress = 300.0f; + lua_pushboolean(L, 1); + return 1; + }); + lua_setfield(L, -2, "conceive"); + + // abort_pregnancy(motherId) + lua_pushcfunction(L, [](lua_State *L) -> int { + uint64_t motherId = + static_cast(lua_tointeger(L, 1)); + auto it = s_stubCharacters.find(motherId); + if (it != s_stubCharacters.end()) { + it->second.pregnantByFatherId = 0; + it->second.pregnancyProgress = 0.0f; + it->second.pregnancyMaxProgress = 0.0f; + } + return 0; + }); + lua_setfield(L, -2, "abort_pregnancy"); + + // is_pregnant(motherId) + lua_pushcfunction(L, [](lua_State *L) -> int { + 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); + 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)); + auto it = s_stubCharacters.find(motherId); + if (it == s_stubCharacters.end() || + it->second.pregnantByFatherId == 0) { + lua_pushnil(L); + return 1; + } + lua_newtable(L); + lua_pushnumber(L, it->second.pregnancyProgress); + lua_setfield(L, -2, "progress"); + 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); + lua_setfield(L, -2, "ratio"); + return 1; + }); + lua_setfield(L, -2, "get_pregnancy_progress"); + + // 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 childId = s_stubNextCharId++; + StubCharacter c; + c.id = childId; + c.firstName = "Baby"; + c.lastName = "Smith"; + c.sex = "female"; + c.persistent = false; + s_stubCharacters[childId] = c; + auto &parents = s_stubParents[childId]; + parents.clear(); + parents.push_back(parentA); + parents.push_back(parentB); + s_stubChildren[parentA].push_back(childId); + s_stubChildren[parentB].push_back(childId); + lua_pushinteger(L, static_cast(childId)); + return 1; + }); + lua_setfield(L, -2, "create_child"); + + // get_parents(childId) + lua_pushcfunction(L, [](lua_State *L) -> int { + 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)); + } + } + return 1; + }); + lua_setfield(L, -2, "get_parents"); + + // get_children(parentId) + lua_pushcfunction(L, [](lua_State *L) -> int { + 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)); + } + } + return 1; + }); + lua_setfield(L, -2, "get_children"); + + lua_setfield(L, -2, "character"); // ecs.character = { ... } + lua_setglobal(L, "ecs"); +} + +} // namespace editScene + // --------------------------------------------------------------------------- // Stub: LuaEntityApi // ---------------------------------------------------------------------------