Now RPG data are in character registry
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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", ¤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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user