diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 846010e..669a839 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -143,12 +143,7 @@ set(EDITSCENE_SOURCES lua/LuaDialogueApi.cpp components/Formula.cpp components/CharacterClassDatabase.cpp - components/CharacterClassComponent.cpp - components/CharacterClassModule.cpp - components/CharacterClassOverrideModule.cpp systems/CharacterClassSystem.cpp - ui/CharacterClassEditor.cpp - ui/CharacterClassOverrideEditor.cpp ui/CharacterClassDatabaseEditor.cpp components/BuoyancyInfoModule.cpp components/WaterPhysicsModule.cpp @@ -203,9 +198,7 @@ set(EDITSCENE_HEADERS lua/LuaDialogueApi.hpp components/Formula.hpp components/CharacterClassDatabase.hpp - components/CharacterClassComponent.hpp - ui/CharacterClassEditor.hpp - ui/CharacterClassOverrideEditor.hpp + ui/CharacterClassDatabaseEditor.hpp systems/CharacterClassSystem.hpp systems/StartupMenuSystem.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index d82f868..0737887 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -32,7 +32,6 @@ #include "systems/DialogueSystem.hpp" #include "systems/CharacterClassSystem.hpp" #include "components/CharacterClassDatabase.hpp" -#include "components/CharacterClassComponent.hpp" #include "systems/PlayerControllerSystem.hpp" #include "systems/SceneSerializer.hpp" #include "camera/EditorCamera.hpp" @@ -724,8 +723,7 @@ void EditorApp::setupECS() m_world.component(); m_world.component(); m_world.component(); - m_world.component(); - m_world.component(); + // Register environment components m_world.component(); diff --git a/src/features/editScene/components/CharacterClassComponent.cpp b/src/features/editScene/components/CharacterClassComponent.cpp deleted file mode 100644 index d669c76..0000000 --- a/src/features/editScene/components/CharacterClassComponent.cpp +++ /dev/null @@ -1,97 +0,0 @@ -#include "CharacterClassComponent.hpp" -#include "CharacterClassDatabase.hpp" - -int CharacterClassComponent::getStat(const Ogre::String &name) const -{ - const auto *def = CharacterClassDatabase::getSingleton().findStat(name); - if (!def) - return 0; - - if (def->kind == CharacterClassDatabase::StatKind::ResourcePool) { - // For pools, getStat returns the CURRENT value - return getPoolCurrent(name); - } - - // For attributes, return the stored value clamped to min/max - auto it = stats.find(name); - if (it == stats.end()) - return 0; - if (it->second < def->minValue) - return def->minValue; - if (it->second > def->maxValue) - return def->maxValue; - return it->second; -} - -int CharacterClassComponent::getPoolMax(const Ogre::String &name) const -{ - const auto *def = CharacterClassDatabase::getSingleton().findStat(name); - if (!def || def->kind != CharacterClassDatabase::StatKind::ResourcePool) - return 0; - auto it = stats.find(name); - if (it == stats.end()) - return 0; - if (it->second < def->minValue) - return def->minValue; - if (it->second > def->maxValue) - return def->maxValue; - return it->second; -} - -int CharacterClassComponent::getPoolCurrent(const Ogre::String &name) const -{ - int maxVal = getPoolMax(name); - if (maxVal <= 0) - return 0; - auto it = currentPools.find(name); - if (it == currentPools.end()) - return maxVal; // not initialized yet, assume full - if (it->second < 0) - return 0; - if (it->second > maxVal) - return maxVal; - return it->second; -} - -void CharacterClassComponent::setPoolCurrent(const Ogre::String &name, - int value) -{ - int maxVal = getPoolMax(name); - if (maxVal <= 0) - return; - if (value < 0) - value = 0; - if (value > maxVal) - value = maxVal; - currentPools[name] = value; -} - -int CharacterClassComponent::getSkill(const Ogre::String &name) const -{ - auto it = skills.find(name); - if (it == skills.end()) - return 0; - const auto *def = CharacterClassDatabase::getSingleton().findSkill(name); - if (!def) - return it->second; - if (it->second < 0) - return 0; - if (it->second > def->maxValue) - return def->maxValue; - return it->second; -} - -int CharacterClassComponent::getNeed(const Ogre::String &name) const -{ - auto it = needs.find(name); - if (it == needs.end()) - return 0; - const auto *def = CharacterClassDatabase::getSingleton().findNeed(name); - if (!def) - return it->second; - if (it->second < 0) - return 0; - if (it->second > def->maxValue) - return def->maxValue; - return it->second; -} diff --git a/src/features/editScene/components/CharacterClassComponent.hpp b/src/features/editScene/components/CharacterClassComponent.hpp deleted file mode 100644 index ac38f6e..0000000 --- a/src/features/editScene/components/CharacterClassComponent.hpp +++ /dev/null @@ -1,107 +0,0 @@ -#ifndef EDITSCENE_CHARACTER_CLASS_COMPONENT_HPP -#define EDITSCENE_CHARACTER_CLASS_COMPONENT_HPP -#pragma once - -#include -#include - -/** - * Runtime character progression state. - * - * Stores the character's current level, XP, available points, - * and current stat/skill/need values. - * - * The class template (base stats, formulas, growth curves) lives in - * the CharacterClassDatabase singleton. This component only holds - * the mutable runtime state for a specific entity. - */ -struct CharacterClassComponent { - /** Class name referencing CharacterClassDatabase */ - Ogre::String className; - - /** Current level (starts at 1, no cap) */ - int level = 1; - - /** Accumulated experience points */ - int64_t currentXP = 0; - - /** Unspent stat points from level ups */ - int availablePoints = 0; - - /** Current stat values (e.g., strength, dexterity, hp_max) */ - std::unordered_map stats; - - /** Current pool values (e.g., hp_current, stamina_current). - * For ResourcePool stats, stats["hp"] is the maximum - * and currentPools["hp"] is the current value. */ - std::unordered_map currentPools; - - /** Current skill values (0-100 proficiency) */ - std::unordered_map skills; - - /** Current need values (0-1000) */ - std::unordered_map needs; - - /** True if the entity has a pending level up (waiting for player) */ - bool levelUpPending = false; - - /** True if the character sheet is open (player only) */ - bool sheetOpen = false; - - /** - * Get a stat value. - * For Attribute: returns the stat value clamped to min/max. - * For ResourcePool: returns the CURRENT pool value. - * Returns 0 if the stat does not exist. - */ - int getStat(const Ogre::String &name) const; - - /** - * Get the maximum value of a resource pool. - * Returns 0 if the stat is not a ResourcePool or does not exist. - */ - int getPoolMax(const Ogre::String &name) const; - - /** - * Get the current value of a resource pool. - * Returns 0 if the stat is not a ResourcePool or does not exist. - */ - int getPoolCurrent(const Ogre::String &name) const; - - /** - * Set the current value of a resource pool. - * Value is clamped to [0, max]. - */ - void setPoolCurrent(const Ogre::String &name, int value); - - /** - * Get a skill value, clamped to 0-100. - * Returns 0 if the skill does not exist. - */ - int getSkill(const Ogre::String &name) const; - - /** - * Get a need value, clamped to 0-1000. - * Returns 0 if the need does not exist. - */ - int getNeed(const Ogre::String &name) const; -}; - -/** - * Per-entity overrides to class defaults. - * - * Applied on top of the class base values during character creation - * or whenever stats are recomputed. - */ -struct CharacterClassOverrideComponent { - /** Flat offsets added to base stats */ - std::unordered_map statOffsets; - - /** Flat offsets added to base skills */ - std::unordered_map skillOffsets; - - /** Flat offsets added to base needs */ - std::unordered_map needOffsets; -}; - -#endif // EDITSCENE_CHARACTER_CLASS_COMPONENT_HPP diff --git a/src/features/editScene/components/CharacterClassModule.cpp b/src/features/editScene/components/CharacterClassModule.cpp deleted file mode 100644 index 10a2129..0000000 --- a/src/features/editScene/components/CharacterClassModule.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "CharacterClassComponent.hpp" -#include "CharacterClassDatabase.hpp" -#include "../ui/ComponentRegistration.hpp" -#include "../ui/CharacterClassEditor.hpp" - -REGISTER_COMPONENT_GROUP("Character Class", "Game", - CharacterClassComponent, CharacterClassEditor) -{ - registry.registerComponent( - "Character Class", "Game", - std::make_unique(), - // Adder - [](flecs::entity e) { - if (!e.has()) - e.set({}); - }, - // Remover - [](flecs::entity e) { - if (e.has()) - e.remove(); - }); -} diff --git a/src/features/editScene/components/CharacterClassOverrideModule.cpp b/src/features/editScene/components/CharacterClassOverrideModule.cpp deleted file mode 100644 index aceaa4f..0000000 --- a/src/features/editScene/components/CharacterClassOverrideModule.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "CharacterClassComponent.hpp" -#include "../ui/ComponentRegistration.hpp" -#include "../ui/CharacterClassOverrideEditor.hpp" - -REGISTER_COMPONENT_GROUP("Character Class Override", "Game", - CharacterClassOverrideComponent, - CharacterClassOverrideEditor) -{ - registry.registerComponent( - "Character Class Override", "Game", - std::make_unique(), - // Adder - [](flecs::entity e) { - if (!e.has()) - e.set({}); - }, - // Remover - [](flecs::entity e) { - if (e.has()) - e.remove(); - }); -} diff --git a/src/features/editScene/lua/LuaCharacterClassApi.cpp b/src/features/editScene/lua/LuaCharacterClassApi.cpp index b33c3aa..91c5653 100644 --- a/src/features/editScene/lua/LuaCharacterClassApi.cpp +++ b/src/features/editScene/lua/LuaCharacterClassApi.cpp @@ -1,8 +1,7 @@ #include "LuaCharacterClassApi.hpp" -#include "../systems/CharacterClassSystem.hpp" +#include "../systems/CharacterRegistry.hpp" #include "../components/CharacterClassDatabase.hpp" -#include "../components/CharacterClassComponent.hpp" -#include "../components/GoapBlackboard.hpp" +#include "../components/CharacterIdentity.hpp" #include namespace editScene @@ -22,6 +21,21 @@ static flecs::world getWorld(lua_State *L) return *world; } +// --------------------------------------------------------------------------- +// Helper: get character record from entity ID +// --------------------------------------------------------------------------- + +static CharacterRegistry::CharacterRecord *getCharacterRecord( + lua_State *L, int entityArgIdx) +{ + int entityId = static_cast(lua_tointeger(L, entityArgIdx)); + flecs::entity e = getWorld(L).entity(entityId); + if (!e.is_alive() || !e.has()) + return nullptr; + auto &ci = e.get(); + return CharacterRegistry::getSingleton().findCharacter(ci.registryId); +} + // --------------------------------------------------------------------------- // Helper: push string-vector as Lua table // --------------------------------------------------------------------------- @@ -111,170 +125,204 @@ static int luaGetStatKind(lua_State *L) } // --------------------------------------------------------------------------- -// Per-entity runtime API +// Per-entity runtime API (via CharacterRegistry) // --------------------------------------------------------------------------- static int luaGetLevel(lua_State *L) { - int entityId = static_cast(lua_tointeger(L, 1)); - flecs::entity e = getWorld(L).entity(entityId); - if (!e.is_alive() || !e.has()) { - lua_pushinteger(L, 0); - return 1; - } - lua_pushinteger(L, e.get().level); + auto *rec = getCharacterRecord(L, 1); + lua_pushinteger(L, rec ? rec->level : 0); return 1; } static int luaGetXP(lua_State *L) { - int entityId = static_cast(lua_tointeger(L, 1)); - flecs::entity e = getWorld(L).entity(entityId); - if (!e.is_alive() || !e.has()) { - lua_pushinteger(L, 0); - return 1; - } - lua_pushinteger(L, - (lua_Integer)e.get().currentXP); + auto *rec = getCharacterRecord(L, 1); + lua_pushinteger(L, rec ? (lua_Integer)rec->currentXP : 0); return 1; } static int luaAddXP(lua_State *L) { - int entityId = static_cast(lua_tointeger(L, 1)); + auto *rec = getCharacterRecord(L, 1); int64_t amount = static_cast(lua_tointeger(L, 2)); - flecs::entity e = getWorld(L).entity(entityId); - if (!e.is_alive() || !e.has()) { + if (!rec) { lua_pushboolean(L, 0); return 1; } - // We can't easily access CharacterClassSystem singleton here, - // so we do the XP add directly and let the system pick it up - auto &cc = e.get_mut(); - cc.currentXP += amount; + rec->currentXP += amount; lua_pushboolean(L, 1); return 1; } static int luaGetStat(lua_State *L) { - int entityId = static_cast(lua_tointeger(L, 1)); + auto *rec = getCharacterRecord(L, 1); const char *statName = lua_tostring(L, 2); - flecs::entity e = getWorld(L).entity(entityId); - if (!e.is_alive() || !e.has() || - !statName) { + if (!rec || !statName) { lua_pushinteger(L, 0); return 1; } - lua_pushinteger(L, - (lua_Integer)e.get().getStat( - statName)); + + const auto *def = CharacterClassDatabase::getSingleton().findStat(statName); + if (def && def->kind == + CharacterClassDatabase::StatKind::ResourcePool) { + int maxVal = rec->stats.count(statName) ? rec->stats[statName] : 0; + if (maxVal <= 0) { + lua_pushinteger(L, 0); + return 1; + } + auto it = rec->currentPools.find(statName); + if (it == rec->currentPools.end()) { + lua_pushinteger(L, maxVal); + return 1; + } + int val = it->second; + if (val < 0) + val = 0; + if (val > maxVal) + val = maxVal; + lua_pushinteger(L, val); + return 1; + } + + lua_pushinteger(L, rec->stats.count(statName) ? rec->stats[statName] : 0); return 1; } static int luaGetSkill(lua_State *L) { - int entityId = static_cast(lua_tointeger(L, 1)); + auto *rec = getCharacterRecord(L, 1); const char *skillName = lua_tostring(L, 2); - flecs::entity e = getWorld(L).entity(entityId); - if (!e.is_alive() || !e.has() || - !skillName) { + if (!rec || !skillName) { lua_pushinteger(L, 0); return 1; } - lua_pushinteger(L, - (lua_Integer)e.get().getSkill( - skillName)); + int val = rec->skills.count(skillName) ? rec->skills[skillName] : 0; + const auto *def = CharacterClassDatabase::getSingleton().findSkill(skillName); + if (def) { + if (val < 0) + val = 0; + if (val > def->maxValue) + val = def->maxValue; + } + lua_pushinteger(L, val); return 1; } static int luaGetNeed(lua_State *L) { - int entityId = static_cast(lua_tointeger(L, 1)); + auto *rec = getCharacterRecord(L, 1); const char *needName = lua_tostring(L, 2); - flecs::entity e = getWorld(L).entity(entityId); - if (!e.is_alive() || !e.has() || - !needName) { + if (!rec || !needName) { lua_pushinteger(L, 0); return 1; } - lua_pushinteger(L, - (lua_Integer)e.get().getNeed( - needName)); + int val = rec->needs.count(needName) ? rec->needs[needName] : 0; + const auto *def = CharacterClassDatabase::getSingleton().findNeed(needName); + if (def) { + if (val < 0) + val = 0; + if (val > def->maxValue) + val = def->maxValue; + } + lua_pushinteger(L, val); return 1; } static int luaGetAvailablePoints(lua_State *L) { - int entityId = static_cast(lua_tointeger(L, 1)); - flecs::entity e = getWorld(L).entity(entityId); - if (!e.is_alive() || !e.has()) { - lua_pushinteger(L, 0); - return 1; - } - lua_pushinteger(L, - (lua_Integer)e.get() - .availablePoints); + auto *rec = getCharacterRecord(L, 1); + lua_pushinteger(L, rec ? (lua_Integer)rec->availablePoints : 0); return 1; } static int luaSetNeed(lua_State *L) { - int entityId = static_cast(lua_tointeger(L, 1)); + auto *rec = getCharacterRecord(L, 1); const char *needName = lua_tostring(L, 2); int value = static_cast(lua_tointeger(L, 3)); - flecs::entity e = getWorld(L).entity(entityId); - if (!e.is_alive() || !e.has() || - !needName) { + if (!rec || !needName) return 0; - } - auto &cc = e.get_mut(); - cc.needs[needName] = value; + rec->needs[needName] = value; return 0; } static int luaGetPoolCurrent(lua_State *L) { - int entityId = static_cast(lua_tointeger(L, 1)); + auto *rec = getCharacterRecord(L, 1); const char *poolName = lua_tostring(L, 2); - flecs::entity e = getWorld(L).entity(entityId); - if (!e.is_alive() || !e.has() || - !poolName) { + if (!rec || !poolName) { lua_pushinteger(L, 0); return 1; } - lua_pushinteger(L, (lua_Integer)e.get(). - getPoolCurrent(poolName)); + int maxVal = rec->stats.count(poolName) ? rec->stats[poolName] : 0; + if (maxVal <= 0) { + lua_pushinteger(L, 0); + return 1; + } + auto it = rec->currentPools.find(poolName); + if (it == rec->currentPools.end()) { + lua_pushinteger(L, maxVal); + return 1; + } + int val = it->second; + if (val < 0) + val = 0; + if (val > maxVal) + val = maxVal; + lua_pushinteger(L, val); return 1; } static int luaGetPoolMax(lua_State *L) { - int entityId = static_cast(lua_tointeger(L, 1)); + auto *rec = getCharacterRecord(L, 1); const char *poolName = lua_tostring(L, 2); - flecs::entity e = getWorld(L).entity(entityId); - if (!e.is_alive() || !e.has() || - !poolName) { + if (!rec || !poolName) { lua_pushinteger(L, 0); return 1; } - lua_pushinteger(L, (lua_Integer)e.get(). - getPoolMax(poolName)); + const auto *def = CharacterClassDatabase::getSingleton().findStat(poolName); + if (!def || def->kind != CharacterClassDatabase::StatKind::ResourcePool) { + lua_pushinteger(L, 0); + return 1; + } + int val = rec->stats.count(poolName) ? rec->stats[poolName] : 0; + if (val < def->minValue) + val = def->minValue; + if (val > def->maxValue) + val = def->maxValue; + lua_pushinteger(L, val); return 1; } static int luaSetPoolCurrent(lua_State *L) { - int entityId = static_cast(lua_tointeger(L, 1)); + auto *rec = getCharacterRecord(L, 1); const char *poolName = lua_tostring(L, 2); int value = static_cast(lua_tointeger(L, 3)); - flecs::entity e = getWorld(L).entity(entityId); - if (!e.is_alive() || !e.has() || - !poolName) { + if (!rec || !poolName) { lua_pushboolean(L, 0); return 1; } - e.get_mut().setPoolCurrent(poolName, value); + int maxVal = rec->stats.count(poolName) ? rec->stats[poolName] : 0; + const auto *def = CharacterClassDatabase::getSingleton().findStat(poolName); + if (def) { + if (maxVal < def->minValue) + maxVal = def->minValue; + if (maxVal > def->maxValue) + maxVal = def->maxValue; + } + if (maxVal <= 0) { + lua_pushboolean(L, 0); + return 1; + } + if (value < 0) + value = 0; + if (value > maxVal) + value = maxVal; + rec->currentPools[poolName] = value; lua_pushboolean(L, 1); return 1; } diff --git a/src/features/editScene/systems/CharacterClassSystem.cpp b/src/features/editScene/systems/CharacterClassSystem.cpp index 17dffd8..a762fd4 100644 --- a/src/features/editScene/systems/CharacterClassSystem.cpp +++ b/src/features/editScene/systems/CharacterClassSystem.cpp @@ -1,7 +1,8 @@ #include "CharacterClassSystem.hpp" #include "../EditorApp.hpp" -#include "../components/CharacterClassComponent.hpp" +#include "CharacterRegistry.hpp" #include "../components/CharacterClassDatabase.hpp" +#include "../components/CharacterIdentity.hpp" #include "../components/GoapBlackboard.hpp" #include "../components/PlayerController.hpp" #include "../components/Inventory.hpp" @@ -9,6 +10,69 @@ #include #include +/* --------------------------------------------------------------------------- + * Helpers + * --------------------------------------------------------------------------- */ + +static CharacterRegistry::CharacterRecord *getRecord(flecs::entity entity) +{ + if (!entity.is_alive() || !entity.has()) + return nullptr; + auto &ci = entity.get(); + return CharacterRegistry::getSingleton().findCharacter(ci.registryId); +} + +static int getPoolMax(const CharacterRegistry::CharacterRecord *rec, + const Ogre::String &name) +{ + if (!rec) + return 0; + const auto *def = CharacterClassDatabase::getSingleton().findStat(name); + if (!def || def->kind != CharacterClassDatabase::StatKind::ResourcePool) + return 0; + auto it = rec->stats.find(name.c_str()); + if (it == rec->stats.end()) + return 0; + if (it->second < def->minValue) + return def->minValue; + if (it->second > def->maxValue) + return def->maxValue; + return it->second; +} + +static int getPoolCurrent(const CharacterRegistry::CharacterRecord *rec, + const Ogre::String &name) +{ + int maxVal = getPoolMax(rec, name); + if (maxVal <= 0) + return 0; + auto it = rec->currentPools.find(name.c_str()); + if (it == rec->currentPools.end()) + return maxVal; + if (it->second < 0) + return 0; + if (it->second > maxVal) + return maxVal; + return it->second; +} + +static void setPoolCurrent(CharacterRegistry::CharacterRecord *rec, + const Ogre::String &name, int value) +{ + int maxVal = getPoolMax(rec, name); + if (maxVal <= 0) + return; + if (value < 0) + value = 0; + if (value > maxVal) + value = maxVal; + rec->currentPools[name.c_str()] = value; +} + +/* --------------------------------------------------------------------------- + * Construction / Destruction + * --------------------------------------------------------------------------- */ + CharacterClassSystem::CharacterClassSystem(flecs::world &world, EditorApp *editorApp) : m_world(world) @@ -20,26 +84,26 @@ CharacterClassSystem::~CharacterClassSystem() { } -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- +/* --------------------------------------------------------------------------- + * Public API + * --------------------------------------------------------------------------- */ bool CharacterClassSystem::addXP(flecs::entity entity, int64_t amount) { - if (!entity.is_alive() || !entity.has()) + auto *rec = getRecord(entity); + if (!rec) return false; - auto &cc = entity.get_mut(); - cc.currentXP += amount; + rec->currentXP += amount; const auto *cls = CharacterClassDatabase::getSingleton().findClass( - cc.className); + rec->className); if (!cls) return false; int64_t needed = CharacterClassDatabase::getSingleton() - .computeXPForLevel(cc.level, *cls); - if (cc.currentXP >= needed) { + .computeXPForLevel(rec->level, *cls); + if (rec->currentXP >= needed) { applyLevelUp(entity); return true; } @@ -49,15 +113,15 @@ bool CharacterClassSystem::addXP(flecs::entity entity, int64_t amount) bool CharacterClassSystem::distributePoint(flecs::entity entity, const Ogre::String &statName) { - if (!entity.is_alive() || !entity.has()) + auto *rec = getRecord(entity); + if (!rec) return false; - auto &cc = entity.get_mut(); - if (cc.availablePoints <= 0) + if (rec->availablePoints <= 0) return false; const auto *cls = CharacterClassDatabase::getSingleton().findClass( - cc.className); + rec->className); if (!cls) return false; @@ -66,134 +130,22 @@ bool CharacterClassSystem::distributePoint(flecs::entity entity, if (!statDef) return false; - int current = cc.getStat(statName); + int current = rec->stats.count(statName.c_str()) ? + rec->stats[statName.c_str()] : + 0; int cost = CharacterClassDatabase::getSingleton().computeStatCost( current, *cls); - if (cc.availablePoints < cost) + if (rec->availablePoints < cost) return false; - cc.availablePoints -= cost; - cc.stats[statName] = current + 1; + rec->availablePoints -= cost; + rec->stats[statName.c_str()] = current + 1; return true; } -void CharacterClassSystem::applyLevelUpGrowthAndPoints(flecs::entity entity) -{ - if (!entity.is_alive() || !entity.has()) - return; - - auto &cc = entity.get_mut(); - const auto *cls = CharacterClassDatabase::getSingleton().findClass( - cc.className); - if (!cls) - return; - - auto &db = CharacterClassDatabase::getSingleton(); - - // Auto-grow stats - for (const auto &pair : cls->statGrowth) { - int growth = db.computeStatGrowth(pair.first, cc.level, *cls); - cc.stats[pair.first] += growth; - } - - // Auto-grow skills - for (const auto &pair : cls->skillGrowth) { - int growth = db.computeSkillGrowth(pair.first, cc.level, *cls); - cc.skills[pair.first] += growth; - const auto *skillDef = db.findSkill(pair.first); - if (skillDef && cc.skills[pair.first] > skillDef->maxValue) - cc.skills[pair.first] = skillDef->maxValue; - } - - // Grant points - cc.availablePoints += db.computePointsForLevel(cc.level, *cls); -} - -void CharacterClassSystem::initializeFromClass(flecs::entity entity) -{ - if (!entity.is_alive() || !entity.has()) - return; - - auto &cc = entity.get_mut(); - const auto *cls = CharacterClassDatabase::getSingleton().findClass( - cc.className); - if (!cls) - return; - - int targetLevel = cc.level; - if (targetLevel < 1) - targetLevel = 1; - - // Reset to base values at level 1 - cc.level = 1; - cc.availablePoints = 0; - - // Base stats + overrides - cc.stats = cls->baseStats; - if (entity.has()) { - const auto &ov = entity.get(); - for (const auto &pair : ov.statOffsets) { - cc.stats[pair.first] += pair.second; - } - } - - // Base skills + overrides - cc.skills = cls->baseSkills; - if (entity.has()) { - const auto &ov = entity.get(); - for (const auto &pair : ov.skillOffsets) { - cc.skills[pair.first] += pair.second; - } - } - - // Base needs + offsets - cc.needs = cls->baseNeeds; - if (entity.has()) { - const auto &ov = entity.get(); - for (const auto &pair : ov.needOffsets) { - cc.needs[pair.first] += pair.second; - } - } - - // Ensure every database-defined stat/skill/need exists with a default - auto &db = CharacterClassDatabase::getSingleton(); - for (const auto &name : db.getStatNames()) { - if (cc.stats.find(name) == cc.stats.end()) { - const auto *def = db.findStat(name); - cc.stats[name] = def ? def->minValue : 1; - } - } - for (const auto &name : db.getSkillNames()) { - if (cc.skills.find(name) == cc.skills.end()) - cc.skills[name] = 0; - } - for (const auto &name : db.getNeedNames()) { - if (cc.needs.find(name) == cc.needs.end()) - cc.needs[name] = 0; - } - - // Simulate level-ups from 2 to targetLevel - for (int lvl = 2; lvl <= targetLevel; ++lvl) { - cc.level = lvl; - applyLevelUpGrowthAndPoints(entity); - distributePointsAI(entity); - } - - // All points should have been spent during simulation - cc.availablePoints = 0; - - // Initialize resource pools to full - for (const auto &name : db.getStatNames()) { - const auto *def = db.findStat(name); - if (def && def->kind == - CharacterClassDatabase::StatKind::ResourcePool) - cc.currentPools[name] = cc.getPoolMax(name); - } -} - -// --------------------------------------------------------------------------- -// Update -// --------------------------------------------------------------------------- +/* --------------------------------------------------------------------------- + * Update + * --------------------------------------------------------------------------- */ void CharacterClassSystem::update(float deltaTime) { @@ -205,13 +157,17 @@ void CharacterClassSystem::accumulateNeeds(float deltaTime) { auto &db = CharacterClassDatabase::getSingleton(); - m_world.query().each( - [&](flecs::entity e, CharacterClassComponent &cc) { - const auto *cls = db.findClass(cc.className); + m_world.query().each( + [&](flecs::entity e, CharacterIdentityComponent &ci) { + auto *rec = CharacterRegistry::getSingleton().findCharacter( + ci.registryId); + if (!rec || rec->className.empty()) + return; + const auto *cls = db.findClass(rec->className); if (!cls) return; - for (auto &pair : cc.needs) { + for (auto &pair : rec->needs) { const auto *needDef = db.findNeed(pair.first); if (!needDef) continue; @@ -230,10 +186,10 @@ void CharacterClassSystem::accumulateNeeds(float deltaTime) void CharacterClassSystem::updateNeedBits(flecs::entity entity) { - if (!entity.has()) + auto *rec = getRecord(entity); + if (!rec) return; - auto &cc = entity.get_mut(); auto &db = CharacterClassDatabase::getSingleton(); if (!entity.has()) @@ -241,7 +197,7 @@ void CharacterClassSystem::updateNeedBits(flecs::entity entity) auto &bb = entity.get_mut(); - for (const auto &pair : cc.needs) { + for (const auto &pair : rec->needs) { const auto *needDef = db.findNeed(pair.first); if (!needDef || needDef->bitName.empty()) continue; @@ -264,19 +220,21 @@ void CharacterClassSystem::updateNeedBits(flecs::entity entity) void CharacterClassSystem::checkLevelUps() { - m_world.query().each( - [&](flecs::entity e, CharacterClassComponent &cc) { - if (cc.levelUpPending) + m_world.query().each( + [&](flecs::entity e, CharacterIdentityComponent &ci) { + auto *rec = CharacterRegistry::getSingleton().findCharacter( + ci.registryId); + if (!rec || rec->levelUpPending || rec->className.empty()) return; const auto *cls = CharacterClassDatabase::getSingleton() - .findClass(cc.className); + .findClass(rec->className); if (!cls) return; int64_t needed = CharacterClassDatabase::getSingleton() - .computeXPForLevel(cc.level, *cls); - if (cc.currentXP >= needed) { + .computeXPForLevel(rec->level, *cls); + if (rec->currentXP >= needed) { applyLevelUp(e); } }); @@ -284,19 +242,19 @@ void CharacterClassSystem::checkLevelUps() void CharacterClassSystem::applyLevelUp(flecs::entity entity) { - if (!entity.is_alive() || !entity.has()) + auto *rec = getRecord(entity); + if (!rec || rec->className.empty()) return; - auto &cc = entity.get_mut(); const auto *cls = CharacterClassDatabase::getSingleton().findClass( - cc.className); + rec->className); if (!cls) return; int64_t needed = CharacterClassDatabase::getSingleton().computeXPForLevel( - cc.level, *cls); - cc.currentXP -= needed; - cc.level++; + rec->level, *cls); + rec->currentXP -= needed; + rec->level++; applyLevelUpGrowthAndPoints(entity); @@ -306,7 +264,7 @@ void CharacterClassSystem::applyLevelUp(flecs::entity entity) m_editorApp->getGameMode() == EditorApp::GameMode::Game && m_editorApp->getGamePlayState() == EditorApp::GamePlayState::Playing) { - cc.levelUpPending = true; + rec->levelUpPending = true; m_levelUpDialogs.insert(entity.id()); } else { // AI: auto-distribute immediately @@ -316,21 +274,53 @@ void CharacterClassSystem::applyLevelUp(flecs::entity entity) Ogre::LogManager::getSingleton().logMessage( Ogre::String("CharacterClassSystem: ") + Ogre::String(entity.name()) + - " reached level " + std::to_string(cc.level) + "!"); + " reached level " + std::to_string(rec->level) + "!"); +} + +void CharacterClassSystem::applyLevelUpGrowthAndPoints(flecs::entity entity) +{ + auto *rec = getRecord(entity); + if (!rec || rec->className.empty()) + return; + + const auto *cls = CharacterClassDatabase::getSingleton().findClass( + rec->className); + if (!cls) + return; + + auto &db = CharacterClassDatabase::getSingleton(); + + // Auto-grow stats + for (const auto &pair : cls->statGrowth) { + int growth = db.computeStatGrowth(pair.first, rec->level, *cls); + rec->stats[pair.first.c_str()] += growth; + } + + // Auto-grow skills + for (const auto &pair : cls->skillGrowth) { + int growth = db.computeSkillGrowth(pair.first, rec->level, *cls); + rec->skills[pair.first.c_str()] += growth; + const auto *skillDef = db.findSkill(pair.first); + if (skillDef && rec->skills[pair.first.c_str()] > skillDef->maxValue) + rec->skills[pair.first.c_str()] = skillDef->maxValue; + } + + // Grant points + rec->availablePoints += db.computePointsForLevel(rec->level, *cls); } void CharacterClassSystem::distributePointsAI(flecs::entity entity) { - if (!entity.is_alive() || !entity.has()) + auto *rec = getRecord(entity); + if (!rec || rec->className.empty()) return; - auto &cc = entity.get_mut(); const auto *cls = CharacterClassDatabase::getSingleton().findClass( - cc.className); + rec->className); if (!cls) return; - int points = cc.availablePoints; + int points = rec->availablePoints; // Round-robin primary stats int idx = 0; @@ -340,11 +330,13 @@ void CharacterClassSystem::distributePointsAI(flecs::entity entity) safety++; const auto &statName = cls->primaryStats[idx % cls->primaryStats.size()]; - int current = cc.getStat(statName); + int current = rec->stats.count(statName.c_str()) ? + rec->stats[statName.c_str()] : + 0; int cost = CharacterClassDatabase::getSingleton() .computeStatCost(current, *cls); if (points >= cost) { - cc.stats[statName] = current + 1; + rec->stats[statName.c_str()] = current + 1; points -= cost; idx++; } else { @@ -355,9 +347,9 @@ void CharacterClassSystem::distributePointsAI(flecs::entity entity) } // Random distribution for remainder - if (points > 0 && !cc.stats.empty()) { - std::vector statNames; - for (const auto &pair : cc.stats) + if (points > 0 && !rec->stats.empty()) { + std::vector statNames; + for (const auto &pair : rec->stats) statNames.push_back(pair.first); int randomSafety = 0; @@ -366,11 +358,13 @@ void CharacterClassSystem::distributePointsAI(flecs::entity entity) randomSafety++; size_t r = rand() % statNames.size(); const auto &statName = statNames[r]; - int current = cc.getStat(statName); + int current = rec->stats.count(statName) ? + rec->stats[statName] : + 0; int cost = CharacterClassDatabase::getSingleton() .computeStatCost(current, *cls); if (points >= cost) { - cc.stats[statName] = current + 1; + rec->stats[statName] = current + 1; points -= cost; } else { // Can't afford this stat, remove from pool @@ -379,7 +373,7 @@ void CharacterClassSystem::distributePointsAI(flecs::entity entity) } } - cc.availablePoints = points; + rec->availablePoints = points; } // --------------------------------------------------------------------------- @@ -392,7 +386,7 @@ void CharacterClassSystem::renderDialogs() std::vector closedDialogs; for (flecs::entity_t id : m_levelUpDialogs) { flecs::entity e = m_world.entity(id); - if (!e.is_alive() || !e.has()) { + if (!e.is_alive() || !e.has()) { closedDialogs.push_back(id); continue; } @@ -405,7 +399,7 @@ void CharacterClassSystem::renderDialogs() std::vector closedSheets; for (flecs::entity_t id : m_sheets) { flecs::entity e = m_world.entity(id); - if (!e.is_alive() || !e.has()) { + if (!e.is_alive() || !e.has()) { closedSheets.push_back(id); continue; } @@ -417,12 +411,12 @@ void CharacterClassSystem::renderDialogs() void CharacterClassSystem::renderLevelUpDialog(flecs::entity entity) { - if (!entity.has()) + auto *rec = getRecord(entity); + if (!rec || rec->className.empty()) return; - auto &cc = entity.get_mut(); const auto *cls = CharacterClassDatabase::getSingleton().findClass( - cc.className); + rec->className); if (!cls) return; @@ -431,26 +425,26 @@ void CharacterClassSystem::renderLevelUpDialog(flecs::entity entity) ImGuiCond_FirstUseEver); Ogre::String title = "Level Up! (Level " + - std::to_string(cc.level) + ")"; + std::to_string(rec->level) + ")"; bool open = true; if (!ImGui::Begin(title.c_str(), &open)) { ImGui::End(); return; } - ImGui::Text("Available Points: %d", cc.availablePoints); + ImGui::Text("Available Points: %d", rec->availablePoints); ImGui::Separator(); auto &db = CharacterClassDatabase::getSingleton(); // Stats - if (!cc.stats.empty()) { + if (!rec->stats.empty()) { ImGui::Text("Stats"); - for (auto &pair : cc.stats) { + for (auto &pair : rec->stats) { const auto *statDef = db.findStat(pair.first); int current = pair.second; int cost = db.computeStatCost(current, *cls); - bool canAfford = cc.availablePoints >= cost; + bool canAfford = rec->availablePoints >= cost; ImGui::PushID(pair.first.c_str()); ImGui::Text("%s: %d", pair.first.c_str(), current); @@ -458,7 +452,7 @@ void CharacterClassSystem::renderLevelUpDialog(flecs::entity entity) ImGui::Text("Cost: %d", cost); ImGui::SameLine(220); if (ImGui::Button("+", ImVec2(30, 0)) && canAfford) { - cc.availablePoints -= cost; + rec->availablePoints -= cost; pair.second = current + 1; } ImGui::PopID(); @@ -468,7 +462,7 @@ void CharacterClassSystem::renderLevelUpDialog(flecs::entity entity) ImGui::Separator(); if (ImGui::Button("Confirm", ImVec2(100, 0))) { - cc.levelUpPending = false; + rec->levelUpPending = false; open = false; } ImGui::SameLine(); @@ -480,18 +474,18 @@ void CharacterClassSystem::renderLevelUpDialog(flecs::entity entity) if (!open) { m_levelUpDialogs.erase(entity.id()); - cc.levelUpPending = false; + rec->levelUpPending = false; } } void CharacterClassSystem::renderCharacterSheet(flecs::entity entity) { - if (!entity.has()) + auto *rec = getRecord(entity); + if (!rec) return; - auto &cc = entity.get_mut(); const auto *cls = CharacterClassDatabase::getSingleton().findClass( - cc.className); + rec->className); Ogre::String title = "Character Sheet"; bool open = true; @@ -505,34 +499,43 @@ void CharacterClassSystem::renderCharacterSheet(flecs::entity entity) } ImGui::Text("Class: %s", - cls ? cls->name.c_str() : cc.className.c_str()); - ImGui::Text("Level: %d", cc.level); - ImGui::Text("XP: %ld", (long)cc.currentXP); - ImGui::Text("Available Points: %d", cc.availablePoints); + cls ? cls->name.c_str() : rec->className.c_str()); + ImGui::Text("Level: %d", rec->level); + ImGui::Text("XP: %ld", (long)rec->currentXP); + ImGui::Text("Available Points: %d", rec->availablePoints); ImGui::Separator(); // Stats - if (!cc.stats.empty()) { + if (!rec->stats.empty()) { ImGui::Text("Stats"); - for (const auto &pair : cc.stats) { - ImGui::Text(" %s: %d", pair.first.c_str(), - pair.second); + for (const auto &pair : rec->stats) { + const auto *def = CharacterClassDatabase::getSingleton() + .findStat(pair.first); + if (def && def->kind == + CharacterClassDatabase::StatKind::ResourcePool) { + int cur = getPoolCurrent(rec, pair.first); + ImGui::Text(" %s: %d / %d", pair.first.c_str(), + cur, pair.second); + } else { + ImGui::Text(" %s: %d", pair.first.c_str(), + pair.second); + } } } // Skills - if (!cc.skills.empty()) { + if (!rec->skills.empty()) { ImGui::Text("Skills"); - for (const auto &pair : cc.skills) { + for (const auto &pair : rec->skills) { ImGui::Text(" %s: %d", pair.first.c_str(), pair.second); } } // Needs - if (!cc.needs.empty()) { + if (!rec->needs.empty()) { ImGui::Text("Needs"); - for (const auto &pair : cc.needs) { + for (const auto &pair : rec->needs) { ImGui::Text(" %s: %d", pair.first.c_str(), pair.second); } diff --git a/src/features/editScene/systems/CharacterClassSystem.hpp b/src/features/editScene/systems/CharacterClassSystem.hpp index 7545889..243668b 100644 --- a/src/features/editScene/systems/CharacterClassSystem.hpp +++ b/src/features/editScene/systems/CharacterClassSystem.hpp @@ -17,6 +17,9 @@ class EditorApp; * - Checks XP and triggers level-ups. * - AI entities auto-distribute stat points. * - Player entities get a level-up dialog. + * + * All mutable RPG data lives in the CharacterRegistry table; + * this system looks it up via CharacterIdentityComponent. */ class CharacterClassSystem { public: @@ -35,16 +38,13 @@ public: /** Distribute one point to a stat (player manual or scripted). */ bool distributePoint(flecs::entity entity, const Ogre::String &statName); - /** Compute initial stats/skills/needs from class + overrides. */ - static void initializeFromClass(flecs::entity entity); - private: void accumulateNeeds(float deltaTime); void updateNeedBits(flecs::entity entity); void checkLevelUps(); void applyLevelUp(flecs::entity entity); - static void applyLevelUpGrowthAndPoints(flecs::entity entity); - static void distributePointsAI(flecs::entity entity); + void applyLevelUpGrowthAndPoints(flecs::entity entity); + void distributePointsAI(flecs::entity entity); void renderLevelUpDialog(flecs::entity entity); void renderCharacterSheet(flecs::entity entity); diff --git a/src/features/editScene/systems/CharacterRegistry.cpp b/src/features/editScene/systems/CharacterRegistry.cpp index cbc3e00..94802de 100644 --- a/src/features/editScene/systems/CharacterRegistry.cpp +++ b/src/features/editScene/systems/CharacterRegistry.cpp @@ -3,7 +3,6 @@ #include "EditorUISystem.hpp" #include "../components/CharacterIdentity.hpp" #include "../components/CharacterSlots.hpp" -#include "../components/CharacterClassComponent.hpp" #include "../components/CharacterClassDatabase.hpp" #include #include @@ -11,12 +10,30 @@ #include #include +/* ===================================================================== */ +/* Singleton */ +/* ===================================================================== */ + +CharacterRegistry *CharacterRegistry::ms_singleton = nullptr; + +CharacterRegistry &CharacterRegistry::getSingleton() +{ + OgreAssert(ms_singleton, "CharacterRegistry not created"); + return *ms_singleton; +} + +CharacterRegistry *CharacterRegistry::getSingletonPtr() +{ + return ms_singleton; +} + /* ===================================================================== */ /* Construction / init */ /* ===================================================================== */ CharacterRegistry::CharacterRegistry() { + ms_singleton = this; m_autoSavePath = "character_registry.json"; } @@ -159,6 +176,124 @@ void CharacterRegistry::scanTemplates() /* Spawn / Save */ /* ===================================================================== */ +void CharacterRegistry::initializeFromClass(uint64_t id) +{ + CharacterRecord *c = findCharacter(id); + if (!c || c->className.empty()) + return; + + const auto *cls = CharacterClassDatabase::getSingleton().findClass( + c->className); + if (!cls) + return; + + int targetLevel = c->level; + if (targetLevel < 1) + targetLevel = 1; + + c->level = 1; + c->availablePoints = 0; + c->stats = cls->baseStats; + c->skills = cls->baseSkills; + c->needs = cls->baseNeeds; + + auto &db = CharacterClassDatabase::getSingleton(); + for (const auto &name : db.getStatNames()) { + if (c->stats.find(name.c_str()) == c->stats.end()) { + const auto *def = db.findStat(name); + c->stats[name.c_str()] = def ? def->minValue : 1; + } + } + for (const auto &name : db.getSkillNames()) { + if (c->skills.find(name.c_str()) == c->skills.end()) + c->skills[name.c_str()] = 0; + } + for (const auto &name : db.getNeedNames()) { + if (c->needs.find(name.c_str()) == c->needs.end()) + c->needs[name.c_str()] = 0; + } + + /* Simulate level-ups */ + for (int lvl = 2; lvl <= targetLevel; ++lvl) { + c->level = lvl; + for (const auto &pair : cls->statGrowth) { + int growth = db.computeStatGrowth(pair.first, c->level, *cls); + c->stats[pair.first.c_str()] += growth; + } + for (const auto &pair : cls->skillGrowth) { + int growth = db.computeSkillGrowth(pair.first, c->level, *cls); + c->skills[pair.first.c_str()] += growth; + const auto *skillDef = db.findSkill(pair.first); + if (skillDef && c->skills[pair.first.c_str()] > skillDef->maxValue) + c->skills[pair.first.c_str()] = skillDef->maxValue; + } + c->availablePoints += db.computePointsForLevel(c->level, *cls); + } + + /* Distribute points AI-style */ + int points = c->availablePoints; + int idx = 0; + int safety = 0; + while (points > 0 && !cls->primaryStats.empty() && + safety < 1000) { + safety++; + const auto &statName = + cls->primaryStats[idx % cls->primaryStats.size()]; + int current = c->stats.count(statName.c_str()) ? + c->stats[statName.c_str()] : + 0; + int cost = db.computeStatCost(current, *cls); + if (points >= cost) { + c->stats[statName.c_str()] = current + 1; + points -= cost; + idx++; + } else { + idx++; + if (idx >= (int)cls->primaryStats.size() * 2) + break; + } + } + if (points > 0 && !c->stats.empty()) { + std::vector statNames; + for (const auto &pair : c->stats) + statNames.push_back(pair.first); + int randomSafety = 0; + while (points > 0 && !statNames.empty() && + randomSafety < 1000) { + randomSafety++; + size_t r = rand() % statNames.size(); + const auto &statName = statNames[r]; + int current = c->stats.count(statName) ? + c->stats[statName] : + 0; + int cost = db.computeStatCost(current, *cls); + if (points >= cost) { + c->stats[statName] = current + 1; + points -= cost; + } else { + statNames.erase(statNames.begin() + r); + } + } + } + c->availablePoints = points; + + /* Initialize resource pools to full */ + for (const auto &name : db.getStatNames()) { + const auto *def = db.findStat(name); + if (def && def->kind == + CharacterClassDatabase::StatKind::ResourcePool) { + int maxVal = c->stats.count(name.c_str()) ? + c->stats[name.c_str()] : + def->minValue; + if (maxVal < def->minValue) + maxVal = def->minValue; + if (maxVal > def->maxValue) + maxVal = def->maxValue; + c->currentPools[name.c_str()] = maxVal; + } + } +} + flecs::entity CharacterRegistry::findSpawnedEntity(uint64_t id) const { if (!m_world) @@ -206,20 +341,6 @@ flecs::entity CharacterRegistry::spawnCharacter(uint64_t id) if (inst.is_alive()) { inst.set( CharacterIdentityComponent{id}); - - /* Apply RPG data from registry */ - if (!c->className.empty()) { - CharacterClassComponent cc; - cc.className = c->className; - cc.level = c->level; - cc.currentXP = c->currentXP; - cc.availablePoints = c->availablePoints; - cc.stats = c->stats; - cc.skills = c->skills; - cc.needs = c->needs; - inst.set(cc); - } - m_uiSystem->addEntity(inst); } return inst; @@ -636,12 +757,15 @@ nlohmann::json CharacterRegistry::serialize() const rec["level"] = c.level; rec["currentXP"] = c.currentXP; rec["availablePoints"] = c.availablePoints; + rec["levelUpPending"] = c.levelUpPending; for (const auto &kv : c.stats) rec["stats"][kv.first] = kv.second; for (const auto &kv : c.skills) rec["skills"][kv.first] = kv.second; for (const auto &kv : c.needs) rec["needs"][kv.first] = kv.second; + for (const auto &kv : c.currentPools) + rec["currentPools"][kv.first] = kv.second; for (const auto &t : c.tags) rec["tags"].push_back(t); for (const auto &kv : c.intColumns) @@ -729,6 +853,7 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) c.level = rec.value("level", 1); c.currentXP = rec.value("currentXP", 0); c.availablePoints = rec.value("availablePoints", 0); + c.levelUpPending = rec.value("levelUpPending", false); if (rec.contains("stats")) { for (auto &[k, v] : rec["stats"].items()) c.stats[k] = v.get(); @@ -741,6 +866,10 @@ void CharacterRegistry::deserialize(const nlohmann::json &j) for (auto &[k, v] : rec["needs"].items()) c.needs[k] = v.get(); } + if (rec.contains("currentPools")) { + for (auto &[k, v] : rec["currentPools"].items()) + c.currentPools[k] = v.get(); + } if (rec.contains("tags")) { for (const auto &t : rec["tags"]) c.tags.push_back(t.get()); @@ -1160,7 +1289,10 @@ void CharacterRegistry::drawEditor(bool *p_open) classNames[i].c_str(), clsIdx == static_cast(i))) { + bool hadClass = !c->className.empty(); c->className = classNames[i].c_str(); + if (!hadClass || c->stats.empty()) + initializeFromClass(c->id); autoSave(); } } @@ -1231,6 +1363,26 @@ void CharacterRegistry::drawEditor(bool *p_open) ImGui::TreePop(); } + /* Current Pools */ + if (ImGui::TreeNode("Current Pools")) { + for (const auto &name : db.getStatNames()) { + const auto *def = db.findStat(name); + if (!def || def->kind != + CharacterClassDatabase::StatKind:: + ResourcePool) + continue; + int val = 0; + auto it = c->currentPools.find(name.c_str()); + if (it != c->currentPools.end()) + val = it->second; + if (ImGui::InputInt(name.c_str(), &val)) { + c->currentPools[name.c_str()] = val; + autoSave(); + } + } + ImGui::TreePop(); + } + /* Tags */ ImGui::Separator(); ImGui::Text("Tags"); diff --git a/src/features/editScene/systems/CharacterRegistry.hpp b/src/features/editScene/systems/CharacterRegistry.hpp index ddf791d..ee039bb 100644 --- a/src/features/editScene/systems/CharacterRegistry.hpp +++ b/src/features/editScene/systems/CharacterRegistry.hpp @@ -24,6 +24,13 @@ class EditorUISystem; * name, created from templates, and deleted along with the character. */ class CharacterRegistry { +public: + static CharacterRegistry &getSingleton(); + static CharacterRegistry *getSingletonPtr(); + +private: + static CharacterRegistry *ms_singleton; + public: /* ------------------------------------------------------------------ */ /* Column schema */ @@ -43,7 +50,7 @@ public: std::string lastName; std::string prefabPath; /* relative path to prefab JSON */ - /* RPG data (supersedes per-entity CharacterClassComponent) */ + /* RPG data (authoritative source for class, level, stats, etc.) */ std::string className; int level = 1; int64_t currentXP = 0; @@ -51,6 +58,8 @@ public: std::unordered_map stats; std::unordered_map skills; std::unordered_map needs; + std::unordered_map currentPools; + bool levelUpPending = false; /* Tags */ std::vector tags; @@ -214,6 +223,13 @@ public: flecs::entity spawnCharacter(uint64_t id); bool savePrefabForCharacter(uint64_t id); + /** + * Initialize RPG data from class definition. + * Resets stats/skills/needs to class base, simulates level-ups, + * and sets pools to full. + */ + void initializeFromClass(uint64_t id); + /* ------------------------------------------------------------------ */ /* Persistence */ /* ------------------------------------------------------------------ */ diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 4c40da6..053b918 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -46,7 +46,6 @@ #include "../components/PrefabInstance.hpp" #include "../components/Item.hpp" #include "../components/Inventory.hpp" -#include "../components/CharacterClassComponent.hpp" #include "../ui/TransformEditor.hpp" #include "../ui/RenderableEditor.hpp" @@ -1191,20 +1190,7 @@ void EditorUISystem::renderComponentList(flecs::entity entity) componentCount++; } - // Render CharacterClass if present - if (entity.has()) { - auto &cc = entity.get_mut(); - m_componentRegistry.render(entity, cc); - componentCount++; - } - // Render CharacterClassOverride if present - if (entity.has()) { - auto &cco = entity.get_mut(); - m_componentRegistry.render( - entity, cco); - componentCount++; - } // Show message if no components diff --git a/src/features/editScene/ui/CharacterClassEditor.cpp b/src/features/editScene/ui/CharacterClassEditor.cpp deleted file mode 100644 index 84c566f..0000000 --- a/src/features/editScene/ui/CharacterClassEditor.cpp +++ /dev/null @@ -1,151 +0,0 @@ -#include "CharacterClassEditor.hpp" -#include "../components/CharacterClassDatabase.hpp" -#include "../systems/CharacterClassSystem.hpp" -#include - -bool CharacterClassEditor::renderComponent(flecs::entity entity, - CharacterClassComponent &comp) -{ - (void)entity; - - auto &db = CharacterClassDatabase::getSingleton(); - - // Class selector - const auto &classNames = db.getClassNames(); - if (!classNames.empty()) { - int selected = -1; - for (int i = 0; i < (int)classNames.size(); i++) { - if (classNames[i] == comp.className) { - selected = i; - break; - } - } - if (ImGui::Combo("Class", &selected, - [](void *data, int idx) -> const char * { - const auto *names = - static_cast *>( - data); - if (idx < 0 || - idx >= (int)names->size()) - return nullptr; - return (*names)[idx].c_str(); - }, - (void *)&classNames, - (int)classNames.size())) { - if (selected >= 0 && - selected < (int)classNames.size()) - comp.className = classNames[selected]; - } - } else { - ImGui::Text("No classes defined in database."); - } - - ImGui::Separator(); - - if (ImGui::Button("Reinitialize from Class")) { - CharacterClassSystem::initializeFromClass(entity); - } - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("Reset stats/skills/needs to class base values,"); - ImGui::Text("then simulate all level-ups with AI point distribution."); - ImGui::EndTooltip(); - } - - // Level & XP - ImGui::InputInt("Level", &comp.level, 1, 5); - if (comp.level < 1) - comp.level = 1; - - int64_t xp = comp.currentXP; - ImGui::InputScalar("Current XP", ImGuiDataType_S64, &xp); - comp.currentXP = xp; - - ImGui::InputInt("Available Points", &comp.availablePoints, 1, 5); - - if (comp.levelUpPending) - ImGui::TextColored(ImVec4(0, 1, 0, 1), "LEVEL UP PENDING!"); - - ImGui::Separator(); - - // Stats: show ALL database stat definitions, not just stored ones - if (!db.getStatNames().empty()) { - ImGui::Text("Stats"); - for (const auto &name : db.getStatNames()) { - const auto *def = db.findStat(name); - if (!def) - continue; - - ImGui::PushID(name.c_str()); - if (def->kind == - CharacterClassDatabase::StatKind::ResourcePool) { - // Resource pool: current / max - int current = comp.getPoolCurrent(name); - int maxVal = comp.stats.count(name) ? - comp.stats.at(name) : - 0; - ImGui::Text("%s", name.c_str()); - ImGui::SameLine(100); - ImGui::Text("Cur:"); - ImGui::SameLine(); - if (ImGui::InputInt("##cur", ¤t, 1, 5)) - comp.setPoolCurrent(name, current); - ImGui::SameLine(); - ImGui::Text("Max:"); - ImGui::SameLine(); - if (ImGui::InputInt("##max", &maxVal, 1, 5)) { - comp.stats[name] = maxVal; - comp.setPoolCurrent(name, - comp.getPoolCurrent( - name)); - } - } else { - // Attribute - int val = comp.stats.count(name) ? - comp.stats.at(name) : - 0; - if (ImGui::InputInt(name.c_str(), &val, 1, 5)) - comp.stats[name] = val; - } - ImGui::PopID(); - } - } - - // Skills: show ALL database skill definitions - if (!db.getSkillNames().empty()) { - ImGui::Separator(); - ImGui::Text("Skills"); - for (const auto &name : db.getSkillNames()) { - int val = comp.skills.count(name) ? - comp.skills.at(name) : - 0; - if (ImGui::InputInt(name.c_str(), &val, 1, 5)) { - if (val < 0) - val = 0; - if (val > 100) - val = 100; - comp.skills[name] = val; - } - } - } - - // Needs: show ALL database need definitions - if (!db.getNeedNames().empty()) { - ImGui::Separator(); - ImGui::Text("Needs"); - for (const auto &name : db.getNeedNames()) { - int val = comp.needs.count(name) ? - comp.needs.at(name) : - 0; - if (ImGui::InputInt(name.c_str(), &val, 1, 5)) { - if (val < 0) - val = 0; - if (val > 1000) - val = 1000; - comp.needs[name] = val; - } - } - } - - return false; -} diff --git a/src/features/editScene/ui/CharacterClassEditor.hpp b/src/features/editScene/ui/CharacterClassEditor.hpp deleted file mode 100644 index d91310c..0000000 --- a/src/features/editScene/ui/CharacterClassEditor.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef EDITSCENE_CHARACTER_CLASS_EDITOR_HPP -#define EDITSCENE_CHARACTER_CLASS_EDITOR_HPP -#pragma once - -#include "ComponentEditor.hpp" -#include "../components/CharacterClassComponent.hpp" - -class CharacterClassEditor : public ComponentEditor { -public: - const char *getName() const override { return "Character Class"; } - -protected: - bool renderComponent(flecs::entity entity, - CharacterClassComponent &comp) override; -}; - -#endif // EDITSCENE_CHARACTER_CLASS_EDITOR_HPP diff --git a/src/features/editScene/ui/CharacterClassOverrideEditor.cpp b/src/features/editScene/ui/CharacterClassOverrideEditor.cpp deleted file mode 100644 index ef97650..0000000 --- a/src/features/editScene/ui/CharacterClassOverrideEditor.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include "CharacterClassOverrideEditor.hpp" -#include "../components/CharacterClassDatabase.hpp" -#include - -bool CharacterClassOverrideEditor::renderComponent( - flecs::entity entity, CharacterClassOverrideComponent &comp) -{ - (void)entity; - auto &db = CharacterClassDatabase::getSingleton(); - - // --- Stat Offsets --- - ImGui::Text("Stat Offsets"); - if (!comp.statOffsets.empty()) { - for (auto it = comp.statOffsets.begin(); - it != comp.statOffsets.end();) { - ImGui::PushID(it->first.c_str()); - int val = it->second; - ImGui::InputInt(it->first.c_str(), &val, 1, 5); - it->second = val; - ImGui::SameLine(); - if (ImGui::Button("Remove")) - it = comp.statOffsets.erase(it); - else - ++it; - ImGui::PopID(); - } - } else { - ImGui::TextDisabled("No stat offsets."); - } - - // Add stat offset - { - const auto &names = db.getStatNames(); - if (!names.empty()) { - static int selected = 0; - static int valBuf = 0; - ImGui::PushID("add_stat"); - if (selected >= (int)names.size()) - selected = 0; - ImGui::Combo("Stat", &selected, - [](void *data, int idx) -> const char * { - const auto *n = static_cast *>(data); - if (idx < 0 || - idx >= (int)n->size()) - return nullptr; - return (*n)[idx].c_str(); - }, - (void *)&names, (int)names.size()); - ImGui::SameLine(); - ImGui::InputInt("Offset", &valBuf, 1, 5); - ImGui::SameLine(); - if (ImGui::Button("Add")) { - comp.statOffsets[names[selected]] = valBuf; - valBuf = 0; - } - ImGui::PopID(); - } - } - - ImGui::Separator(); - - // --- Skill Offsets --- - ImGui::Text("Skill Offsets"); - if (!comp.skillOffsets.empty()) { - for (auto it = comp.skillOffsets.begin(); - it != comp.skillOffsets.end();) { - ImGui::PushID(it->first.c_str()); - int val = it->second; - ImGui::InputInt(it->first.c_str(), &val, 1, 5); - it->second = val; - ImGui::SameLine(); - if (ImGui::Button("Remove")) - it = comp.skillOffsets.erase(it); - else - ++it; - ImGui::PopID(); - } - } else { - ImGui::TextDisabled("No skill offsets."); - } - - // Add skill offset - { - const auto &names = db.getSkillNames(); - if (!names.empty()) { - static int selected = 0; - static int valBuf = 0; - ImGui::PushID("add_skill"); - if (selected >= (int)names.size()) - selected = 0; - ImGui::Combo("Skill", &selected, - [](void *data, int idx) -> const char * { - const auto *n = static_cast *>(data); - if (idx < 0 || - idx >= (int)n->size()) - return nullptr; - return (*n)[idx].c_str(); - }, - (void *)&names, (int)names.size()); - ImGui::SameLine(); - ImGui::InputInt("Offset", &valBuf, 1, 5); - ImGui::SameLine(); - if (ImGui::Button("Add")) { - comp.skillOffsets[names[selected]] = valBuf; - valBuf = 0; - } - ImGui::PopID(); - } - } - - ImGui::Separator(); - - // --- Need Offsets --- - ImGui::Text("Need Offsets"); - if (!comp.needOffsets.empty()) { - for (auto it = comp.needOffsets.begin(); - it != comp.needOffsets.end();) { - ImGui::PushID(it->first.c_str()); - int val = it->second; - ImGui::InputInt(it->first.c_str(), &val, 1, 5); - it->second = val; - ImGui::SameLine(); - if (ImGui::Button("Remove")) - it = comp.needOffsets.erase(it); - else - ++it; - ImGui::PopID(); - } - } else { - ImGui::TextDisabled("No need offsets."); - } - - // Add need offset - { - const auto &names = db.getNeedNames(); - if (!names.empty()) { - static int selected = 0; - static int valBuf = 0; - ImGui::PushID("add_need"); - if (selected >= (int)names.size()) - selected = 0; - ImGui::Combo("Need", &selected, - [](void *data, int idx) -> const char * { - const auto *n = static_cast *>(data); - if (idx < 0 || - idx >= (int)n->size()) - return nullptr; - return (*n)[idx].c_str(); - }, - (void *)&names, (int)names.size()); - ImGui::SameLine(); - ImGui::InputInt("Offset", &valBuf, 1, 5); - ImGui::SameLine(); - if (ImGui::Button("Add")) { - comp.needOffsets[names[selected]] = valBuf; - valBuf = 0; - } - ImGui::PopID(); - } - } - - return false; -} diff --git a/src/features/editScene/ui/CharacterClassOverrideEditor.hpp b/src/features/editScene/ui/CharacterClassOverrideEditor.hpp deleted file mode 100644 index e272896..0000000 --- a/src/features/editScene/ui/CharacterClassOverrideEditor.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef EDITSCENE_CHARACTER_CLASS_OVERRIDE_EDITOR_HPP -#define EDITSCENE_CHARACTER_CLASS_OVERRIDE_EDITOR_HPP -#pragma once - -#include "ComponentEditor.hpp" -#include "../components/CharacterClassComponent.hpp" - -class CharacterClassOverrideEditor - : public ComponentEditor { -public: - const char *getName() const override - { - return "Character Class Override"; - } - -protected: - bool renderComponent(flecs::entity entity, - CharacterClassOverrideComponent &comp) override; -}; - -#endif // EDITSCENE_CHARACTER_CLASS_OVERRIDE_EDITOR_HPP