Pregnancy and birth

This commit is contained in:
2026-05-13 23:31:59 +03:00
parent 089d13520e
commit ef49506515
11 changed files with 2248 additions and 53 deletions
+20
View File
@@ -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"
+10
View File
@@ -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<CharacterClassSystem>(
m_world, this);
m_pregnancySystem =
std::make_unique<PregnancySystem>(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.
+6
View File
@@ -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<EventHandlerSystem> m_eventHandlerSystem;
std::unique_ptr<ItemSystem> m_itemSystem;
std::unique_ptr<CharacterClassSystem> m_characterClassSystem;
std::unique_ptr<PregnancySystem> m_pregnancySystem;
// Game systems
std::unique_ptr<StartupMenuSystem> m_startupMenuSystem;
@@ -0,0 +1,322 @@
#include "LuaCharacterApi.hpp"
#include "LuaEntityApi.hpp"
#include "../systems/CharacterRegistry.hpp"
#include "../components/CharacterIdentity.hpp"
#include <OgreLogManager.h>
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<flecs::world *>(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<uint64_t> &vec)
{
lua_newtable(L);
for (size_t i = 0; i < vec.size(); i++) {
lua_pushinteger(L, static_cast<lua_Integer>(vec[i]));
lua_rawseti(L, -2, static_cast<int>(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<lua_Integer>(id));
return 1;
}
static int luaCharacterDelete(lua_State *L)
{
uint64_t id = static_cast<uint64_t>(luaL_checkinteger(L, 1));
CharacterRegistry::getSingleton().deleteCharacter(id);
return 0;
}
static int luaCharacterFind(lua_State *L)
{
uint64_t id = static_cast<uint64_t>(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<lua_Integer>(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<lua_Integer>(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<lua_Integer>(pair.first));
lua_rawseti(L, -2, idx++);
}
return 1;
}
static int luaCharacterSetName(lua_State *L)
{
uint64_t id = static_cast<uint64_t>(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<uint64_t>(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<uint64_t>(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<uint64_t>(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<uint64_t>(luaL_checkinteger(L, 1));
uint64_t fatherId =
static_cast<uint64_t>(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<uint64_t>(luaL_checkinteger(L, 1));
CharacterRegistry::getSingleton().abortPregnancy(motherId);
return 0;
}
static int luaCharacterIsPregnant(lua_State *L)
{
uint64_t motherId =
static_cast<uint64_t>(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<uint64_t>(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<uint64_t>(luaL_checkinteger(L, 1));
uint64_t parentB =
static_cast<uint64_t>(luaL_checkinteger(L, 2));
uint64_t childId =
CharacterRegistry::getSingleton().createChild(parentA,
parentB);
lua_pushinteger(L, static_cast<lua_Integer>(childId));
return 1;
}
static int luaCharacterGetParents(lua_State *L)
{
uint64_t childId =
static_cast<uint64_t>(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<uint64_t>(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
@@ -0,0 +1,11 @@
#ifndef EDITSCENE_LUA_CHARACTER_API_HPP
#define EDITSCENE_LUA_CHARACTER_API_HPP
#pragma once
#include <lua.hpp>
namespace editScene {
void registerLuaCharacterApi(lua_State *L);
}
#endif // EDITSCENE_LUA_CHARACTER_API_HPP
File diff suppressed because it is too large Load Diff
@@ -12,6 +12,7 @@
#include <vector>
#include "MarkovNameGenerator.hpp"
#include "../components/CharacterSlots.hpp"
class EditorUISystem;
@@ -63,6 +64,25 @@ public:
std::unordered_map<std::string, int> 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<std::string, SlotSelection> inlineSlotSelections;
std::unordered_map<std::string, float> 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<std::string> 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<std::string> &getMaleFirstNameSeeds()
{
return m_maleFirstNameSeeds;
}
std::vector<std::string> &getFemaleFirstNameSeeds()
{
return m_femaleFirstNameSeeds;
}
std::vector<std::string> &getLastNameSeeds()
{
return m_lastNameSeeds;
}
/* Family / Birth */
/* ------------------------------------------------------------------ */
std::vector<uint64_t> getParents(uint64_t childId) const;
std::vector<uint64_t> 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<std::string> &getBirthRandomizableShapeKeys()
{
return m_birthRandomizableShapeKeys;
}
std::vector<std::string> &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<uint64_t, CharacterRecord> m_characters;
std::unordered_map<uint64_t, GroupRecord> m_groups;
@@ -274,9 +357,23 @@ private:
std::string m_autoSavePath;
std::vector<std::string> m_templates;
MarkovNameGenerator m_firstNameGen;
MarkovNameGenerator m_maleFirstNameGen;
MarkovNameGenerator m_femaleFirstNameGen;
MarkovNameGenerator m_lastNameGen;
std::vector<std::string> m_maleFirstNameSeeds;
std::vector<std::string> m_femaleFirstNameSeeds;
std::vector<std::string> m_lastNameSeeds;
std::vector<std::string> m_birthRandomizableShapeKeys;
std::vector<std::string> 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;
@@ -0,0 +1,79 @@
#include "PregnancySystem.hpp"
#include "CharacterRegistry.hpp"
#include "EventBus.hpp"
#include "../components/EventParams.hpp"
#include <OgreLogManager.h>
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 &reg = 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 &reg = CharacterRegistry::getSingleton();
std::vector<uint64_t> 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));
}
}
@@ -0,0 +1,28 @@
#ifndef EDITSCENE_PREGNANCY_SYSTEM_HPP
#define EDITSCENE_PREGNANCY_SYSTEM_HPP
#pragma once
#include <flecs.h>
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
@@ -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 <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <vector>
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
#include "ogre_stub.h"
// Include Lua
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
// 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 <typename Func> void children(Func) const
{
}
template <typename T> void add()
{
}
template <typename T> bool has() const
{
return false;
}
template <typename T> const T *get() const
{
return nullptr;
}
template <typename T> 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;
}
@@ -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<uint64_t, StubCharacter> s_stubCharacters;
static std::unordered_map<uint64_t, std::vector<uint64_t> > s_stubParents;
static std::unordered_map<uint64_t, std::vector<uint64_t> > 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<lua_Integer>(id));
return 1;
});
lua_setfield(L, -2, "create");
// delete(id)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t id = static_cast<uint64_t>(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<uint64_t>(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<lua_Integer>(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<lua_Integer>(
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<lua_Integer>(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<uint64_t>(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<uint64_t>(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<uint64_t>(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<uint64_t>(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<uint64_t>(lua_tointeger(L, 1));
uint64_t fatherId =
static_cast<uint64_t>(lua_tointeger(L, 2));
auto it = s_stubCharacters.find(motherId);
if (it == s_stubCharacters.end()) {
lua_pushboolean(L, 0);
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<uint64_t>(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<uint64_t>(lua_tointeger(L, 1));
auto it = s_stubCharacters.find(motherId);
lua_pushboolean(L,
(it != s_stubCharacters.end() &&
it->second.pregnantByFatherId != 0) ?
1 :
0);
return 1;
});
lua_setfield(L, -2, "is_pregnant");
// get_pregnancy_progress(motherId)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t motherId =
static_cast<uint64_t>(lua_tointeger(L, 1));
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<uint64_t>(lua_tointeger(L, 1));
uint64_t parentB =
static_cast<uint64_t>(lua_tointeger(L, 2));
uint64_t childId = s_stubNextCharId++;
StubCharacter c;
c.id = childId;
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<lua_Integer>(childId));
return 1;
});
lua_setfield(L, -2, "create_child");
// get_parents(childId)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t childId =
static_cast<uint64_t>(lua_tointeger(L, 1));
lua_newtable(L);
auto it = s_stubParents.find(childId);
if (it != s_stubParents.end()) {
for (size_t i = 0; i < it->second.size(); i++) {
lua_pushinteger(
L,
static_cast<lua_Integer>(
it->second[i]));
lua_rawseti(L, -2,
static_cast<int>(i + 1));
}
}
return 1;
});
lua_setfield(L, -2, "get_parents");
// get_children(parentId)
lua_pushcfunction(L, [](lua_State *L) -> int {
uint64_t parentId =
static_cast<uint64_t>(lua_tointeger(L, 1));
lua_newtable(L);
auto it = s_stubChildren.find(parentId);
if (it != s_stubChildren.end()) {
for (size_t i = 0; i < it->second.size(); i++) {
lua_pushinteger(
L,
static_cast<lua_Integer>(
it->second[i]));
lua_rawseti(L, -2,
static_cast<int>(i + 1));
}
}
return 1;
});
lua_setfield(L, -2, "get_children");
lua_setfield(L, -2, "character"); // ecs.character = { ... }
lua_setglobal(L, "ecs");
}
} // namespace editScene
// ---------------------------------------------------------------------------
// Stub: LuaEntityApi
// ---------------------------------------------------------------------------