Now RPG data are in character registry

This commit is contained in:
2026-05-11 15:03:31 +03:00
parent f9e61dcb05
commit 472af01e94
16 changed files with 518 additions and 925 deletions
+1 -8
View File
@@ -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
+1 -3
View File
@@ -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<StartupMenuComponent>();
m_world.component<PlayerControllerComponent>();
m_world.component<InWater>();
m_world.component<CharacterClassComponent>();
m_world.component<CharacterClassOverrideComponent>();
// Register environment components
m_world.component<SunComponent>();
@@ -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;
}
@@ -1,107 +0,0 @@
#ifndef EDITSCENE_CHARACTER_CLASS_COMPONENT_HPP
#define EDITSCENE_CHARACTER_CLASS_COMPONENT_HPP
#pragma once
#include <Ogre.h>
#include <unordered_map>
/**
* 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<Ogre::String, int> 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<Ogre::String, int> currentPools;
/** Current skill values (0-100 proficiency) */
std::unordered_map<Ogre::String, int> skills;
/** Current need values (0-1000) */
std::unordered_map<Ogre::String, int> 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<Ogre::String, int> statOffsets;
/** Flat offsets added to base skills */
std::unordered_map<Ogre::String, int> skillOffsets;
/** Flat offsets added to base needs */
std::unordered_map<Ogre::String, int> needOffsets;
};
#endif // EDITSCENE_CHARACTER_CLASS_COMPONENT_HPP
@@ -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<CharacterClassComponent>(
"Character Class", "Game",
std::make_unique<CharacterClassEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<CharacterClassComponent>())
e.set<CharacterClassComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<CharacterClassComponent>())
e.remove<CharacterClassComponent>();
});
}
@@ -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<CharacterClassOverrideComponent>(
"Character Class Override", "Game",
std::make_unique<CharacterClassOverrideEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<CharacterClassOverrideComponent>())
e.set<CharacterClassOverrideComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<CharacterClassOverrideComponent>())
e.remove<CharacterClassOverrideComponent>();
});
}
@@ -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 <OgreLogManager.h>
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<int>(lua_tointeger(L, entityArgIdx));
flecs::entity e = getWorld(L).entity(entityId);
if (!e.is_alive() || !e.has<CharacterIdentityComponent>())
return nullptr;
auto &ci = e.get<CharacterIdentityComponent>();
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<int>(lua_tointeger(L, 1));
flecs::entity e = getWorld(L).entity(entityId);
if (!e.is_alive() || !e.has<CharacterClassComponent>()) {
lua_pushinteger(L, 0);
return 1;
}
lua_pushinteger(L, e.get<CharacterClassComponent>().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<int>(lua_tointeger(L, 1));
flecs::entity e = getWorld(L).entity(entityId);
if (!e.is_alive() || !e.has<CharacterClassComponent>()) {
lua_pushinteger(L, 0);
return 1;
}
lua_pushinteger(L,
(lua_Integer)e.get<CharacterClassComponent>().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<int>(lua_tointeger(L, 1));
auto *rec = getCharacterRecord(L, 1);
int64_t amount = static_cast<int64_t>(lua_tointeger(L, 2));
flecs::entity e = getWorld(L).entity(entityId);
if (!e.is_alive() || !e.has<CharacterClassComponent>()) {
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<CharacterClassComponent>();
cc.currentXP += amount;
rec->currentXP += amount;
lua_pushboolean(L, 1);
return 1;
}
static int luaGetStat(lua_State *L)
{
int entityId = static_cast<int>(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<CharacterClassComponent>() ||
!statName) {
if (!rec || !statName) {
lua_pushinteger(L, 0);
return 1;
}
lua_pushinteger(L,
(lua_Integer)e.get<CharacterClassComponent>().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<int>(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<CharacterClassComponent>() ||
!skillName) {
if (!rec || !skillName) {
lua_pushinteger(L, 0);
return 1;
}
lua_pushinteger(L,
(lua_Integer)e.get<CharacterClassComponent>().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<int>(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<CharacterClassComponent>() ||
!needName) {
if (!rec || !needName) {
lua_pushinteger(L, 0);
return 1;
}
lua_pushinteger(L,
(lua_Integer)e.get<CharacterClassComponent>().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<int>(lua_tointeger(L, 1));
flecs::entity e = getWorld(L).entity(entityId);
if (!e.is_alive() || !e.has<CharacterClassComponent>()) {
lua_pushinteger(L, 0);
return 1;
}
lua_pushinteger(L,
(lua_Integer)e.get<CharacterClassComponent>()
.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<int>(lua_tointeger(L, 1));
auto *rec = getCharacterRecord(L, 1);
const char *needName = lua_tostring(L, 2);
int value = static_cast<int>(lua_tointeger(L, 3));
flecs::entity e = getWorld(L).entity(entityId);
if (!e.is_alive() || !e.has<CharacterClassComponent>() ||
!needName) {
if (!rec || !needName)
return 0;
}
auto &cc = e.get_mut<CharacterClassComponent>();
cc.needs[needName] = value;
rec->needs[needName] = value;
return 0;
}
static int luaGetPoolCurrent(lua_State *L)
{
int entityId = static_cast<int>(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<CharacterClassComponent>() ||
!poolName) {
if (!rec || !poolName) {
lua_pushinteger(L, 0);
return 1;
}
lua_pushinteger(L, (lua_Integer)e.get<CharacterClassComponent>().
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<int>(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<CharacterClassComponent>() ||
!poolName) {
if (!rec || !poolName) {
lua_pushinteger(L, 0);
return 1;
}
lua_pushinteger(L, (lua_Integer)e.get<CharacterClassComponent>().
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<int>(lua_tointeger(L, 1));
auto *rec = getCharacterRecord(L, 1);
const char *poolName = lua_tostring(L, 2);
int value = static_cast<int>(lua_tointeger(L, 3));
flecs::entity e = getWorld(L).entity(entityId);
if (!e.is_alive() || !e.has<CharacterClassComponent>() ||
!poolName) {
if (!rec || !poolName) {
lua_pushboolean(L, 0);
return 1;
}
e.get_mut<CharacterClassComponent>().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;
}
@@ -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 <imgui.h>
#include <algorithm>
/* ---------------------------------------------------------------------------
* Helpers
* --------------------------------------------------------------------------- */
static CharacterRegistry::CharacterRecord *getRecord(flecs::entity entity)
{
if (!entity.is_alive() || !entity.has<CharacterIdentityComponent>())
return nullptr;
auto &ci = entity.get<CharacterIdentityComponent>();
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<CharacterClassComponent>())
auto *rec = getRecord(entity);
if (!rec)
return false;
auto &cc = entity.get_mut<CharacterClassComponent>();
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<CharacterClassComponent>())
auto *rec = getRecord(entity);
if (!rec)
return false;
auto &cc = entity.get_mut<CharacterClassComponent>();
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<CharacterClassComponent>())
return;
auto &cc = entity.get_mut<CharacterClassComponent>();
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<CharacterClassComponent>())
return;
auto &cc = entity.get_mut<CharacterClassComponent>();
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<CharacterClassOverrideComponent>()) {
const auto &ov = entity.get<CharacterClassOverrideComponent>();
for (const auto &pair : ov.statOffsets) {
cc.stats[pair.first] += pair.second;
}
}
// Base skills + overrides
cc.skills = cls->baseSkills;
if (entity.has<CharacterClassOverrideComponent>()) {
const auto &ov = entity.get<CharacterClassOverrideComponent>();
for (const auto &pair : ov.skillOffsets) {
cc.skills[pair.first] += pair.second;
}
}
// Base needs + offsets
cc.needs = cls->baseNeeds;
if (entity.has<CharacterClassOverrideComponent>()) {
const auto &ov = entity.get<CharacterClassOverrideComponent>();
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<CharacterClassComponent>().each(
[&](flecs::entity e, CharacterClassComponent &cc) {
const auto *cls = db.findClass(cc.className);
m_world.query<CharacterIdentityComponent>().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<CharacterClassComponent>())
auto *rec = getRecord(entity);
if (!rec)
return;
auto &cc = entity.get_mut<CharacterClassComponent>();
auto &db = CharacterClassDatabase::getSingleton();
if (!entity.has<GoapBlackboard>())
@@ -241,7 +197,7 @@ void CharacterClassSystem::updateNeedBits(flecs::entity entity)
auto &bb = entity.get_mut<GoapBlackboard>();
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<CharacterClassComponent>().each(
[&](flecs::entity e, CharacterClassComponent &cc) {
if (cc.levelUpPending)
m_world.query<CharacterIdentityComponent>().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<CharacterClassComponent>())
auto *rec = getRecord(entity);
if (!rec || rec->className.empty())
return;
auto &cc = entity.get_mut<CharacterClassComponent>();
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<CharacterClassComponent>())
auto *rec = getRecord(entity);
if (!rec || rec->className.empty())
return;
auto &cc = entity.get_mut<CharacterClassComponent>();
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<Ogre::String> statNames;
for (const auto &pair : cc.stats)
if (points > 0 && !rec->stats.empty()) {
std::vector<std::string> 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<flecs::entity_t> closedDialogs;
for (flecs::entity_t id : m_levelUpDialogs) {
flecs::entity e = m_world.entity(id);
if (!e.is_alive() || !e.has<CharacterClassComponent>()) {
if (!e.is_alive() || !e.has<CharacterIdentityComponent>()) {
closedDialogs.push_back(id);
continue;
}
@@ -405,7 +399,7 @@ void CharacterClassSystem::renderDialogs()
std::vector<flecs::entity_t> closedSheets;
for (flecs::entity_t id : m_sheets) {
flecs::entity e = m_world.entity(id);
if (!e.is_alive() || !e.has<CharacterClassComponent>()) {
if (!e.is_alive() || !e.has<CharacterIdentityComponent>()) {
closedSheets.push_back(id);
continue;
}
@@ -417,12 +411,12 @@ void CharacterClassSystem::renderDialogs()
void CharacterClassSystem::renderLevelUpDialog(flecs::entity entity)
{
if (!entity.has<CharacterClassComponent>())
auto *rec = getRecord(entity);
if (!rec || rec->className.empty())
return;
auto &cc = entity.get_mut<CharacterClassComponent>();
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<CharacterClassComponent>())
auto *rec = getRecord(entity);
if (!rec)
return;
auto &cc = entity.get_mut<CharacterClassComponent>();
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);
}
@@ -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);
@@ -3,7 +3,6 @@
#include "EditorUISystem.hpp"
#include "../components/CharacterIdentity.hpp"
#include "../components/CharacterSlots.hpp"
#include "../components/CharacterClassComponent.hpp"
#include "../components/CharacterClassDatabase.hpp"
#include <OgreLogManager.h>
#include <fstream>
@@ -11,12 +10,30 @@
#include <algorithm>
#include <filesystem>
/* ===================================================================== */
/* 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<std::string> 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>(
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<CharacterClassComponent>(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<int>();
@@ -741,6 +866,10 @@ void CharacterRegistry::deserialize(const nlohmann::json &j)
for (auto &[k, v] : rec["needs"].items())
c.needs[k] = v.get<int>();
}
if (rec.contains("currentPools")) {
for (auto &[k, v] : rec["currentPools"].items())
c.currentPools[k] = v.get<int>();
}
if (rec.contains("tags")) {
for (const auto &t : rec["tags"])
c.tags.push_back(t.get<std::string>());
@@ -1160,7 +1289,10 @@ void CharacterRegistry::drawEditor(bool *p_open)
classNames[i].c_str(),
clsIdx ==
static_cast<int>(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");
@@ -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<std::string, int> stats;
std::unordered_map<std::string, int> skills;
std::unordered_map<std::string, int> needs;
std::unordered_map<std::string, int> currentPools;
bool levelUpPending = false;
/* Tags */
std::vector<std::string> 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 */
/* ------------------------------------------------------------------ */
@@ -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<CharacterClassComponent>()) {
auto &cc = entity.get_mut<CharacterClassComponent>();
m_componentRegistry.render<CharacterClassComponent>(entity, cc);
componentCount++;
}
// Render CharacterClassOverride if present
if (entity.has<CharacterClassOverrideComponent>()) {
auto &cco = entity.get_mut<CharacterClassOverrideComponent>();
m_componentRegistry.render<CharacterClassOverrideComponent>(
entity, cco);
componentCount++;
}
// Show message if no components
@@ -1,151 +0,0 @@
#include "CharacterClassEditor.hpp"
#include "../components/CharacterClassDatabase.hpp"
#include "../systems/CharacterClassSystem.hpp"
#include <imgui.h>
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<const std::vector<Ogre::String> *>(
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", &current, 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;
}
@@ -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<CharacterClassComponent> {
public:
const char *getName() const override { return "Character Class"; }
protected:
bool renderComponent(flecs::entity entity,
CharacterClassComponent &comp) override;
};
#endif // EDITSCENE_CHARACTER_CLASS_EDITOR_HPP
@@ -1,166 +0,0 @@
#include "CharacterClassOverrideEditor.hpp"
#include "../components/CharacterClassDatabase.hpp"
#include <imgui.h>
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<const std::vector<
Ogre::String> *>(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<const std::vector<
Ogre::String> *>(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<const std::vector<
Ogre::String> *>(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;
}
@@ -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<CharacterClassOverrideComponent> {
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