Better tag support
This commit is contained in:
@@ -137,9 +137,17 @@ set(EDITSCENE_SOURCES
|
||||
components/CellGrid.cpp
|
||||
components/StartupMenuModule.cpp
|
||||
components/PlayerControllerModule.cpp
|
||||
components/DialogueComponentModule.cpp
|
||||
systems/DialogueSystem.cpp
|
||||
ui/DialogueEditor.cpp
|
||||
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
|
||||
components/WaterPlaneModule.cpp
|
||||
@@ -156,6 +164,7 @@ set(EDITSCENE_SOURCES
|
||||
lua/LuaActionApi.cpp
|
||||
lua/LuaBehaviorTreeApi.cpp
|
||||
lua/LuaGameModeApi.cpp
|
||||
lua/LuaCharacterClassApi.cpp
|
||||
)
|
||||
|
||||
set(EDITSCENE_HEADERS
|
||||
@@ -187,9 +196,15 @@ set(EDITSCENE_HEADERS
|
||||
components/CellGrid.hpp
|
||||
components/StartupMenu.hpp
|
||||
components/PlayerController.hpp
|
||||
components/DialogueComponent.hpp
|
||||
systems/DialogueSystem.hpp
|
||||
ui/DialogueEditor.hpp
|
||||
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
|
||||
systems/PlayerControllerSystem.hpp
|
||||
systems/EditorUISystem.hpp
|
||||
@@ -308,6 +323,7 @@ set(EDITSCENE_HEADERS
|
||||
lua/LuaActionApi.hpp
|
||||
lua/LuaBehaviorTreeApi.hpp
|
||||
lua/LuaGameModeApi.hpp
|
||||
lua/LuaCharacterClassApi.hpp
|
||||
)
|
||||
|
||||
add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include "EditorApp.hpp"
|
||||
#include "GameMode.hpp"
|
||||
#include "systems/EditorUISystem.hpp"
|
||||
@@ -29,6 +30,9 @@
|
||||
#include "systems/RoomLayoutSystem.hpp"
|
||||
#include "systems/StartupMenuSystem.hpp"
|
||||
#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"
|
||||
@@ -60,7 +64,6 @@
|
||||
#include "components/AnimationTreeTemplate.hpp"
|
||||
#include "components/Character.hpp"
|
||||
#include "components/StartupMenu.hpp"
|
||||
#include "components/DialogueComponent.hpp"
|
||||
#include "components/PlayerController.hpp"
|
||||
#include "components/CellGrid.hpp"
|
||||
#include "components/CellGridModule.hpp"
|
||||
@@ -92,6 +95,8 @@
|
||||
#include "lua/LuaActionApi.hpp"
|
||||
#include "lua/LuaBehaviorTreeApi.hpp"
|
||||
#include "lua/LuaGameModeApi.hpp"
|
||||
#include "lua/LuaCharacterClassApi.hpp"
|
||||
#include "lua/LuaDialogueApi.hpp"
|
||||
|
||||
//=============================================================================
|
||||
// ImGuiRenderListener Implementation
|
||||
@@ -144,14 +149,13 @@ void ImGuiRenderListener::preViewportUpdate(
|
||||
sms->update(m_deltaTime);
|
||||
}
|
||||
|
||||
// Render dialogue box in game mode (inside ImGui frame scope)
|
||||
if (m_editorApp &&
|
||||
m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
|
||||
m_editorApp->getGamePlayState() ==
|
||||
EditorApp::GamePlayState::Playing) {
|
||||
DialogueSystem *ds = m_editorApp->getDialogueSystem();
|
||||
if (ds)
|
||||
ds->update(m_deltaTime);
|
||||
// Render dialogue box (game mode or editor preview)
|
||||
DialogueSystem::getInstance().update(m_deltaTime);
|
||||
|
||||
// Character class system (needs, level-ups, dialogs)
|
||||
if (m_editorApp && m_editorApp->getCharacterClassSystem()) {
|
||||
m_editorApp->getCharacterClassSystem()->update(m_deltaTime);
|
||||
m_editorApp->getCharacterClassSystem()->renderDialogs();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,8 +203,7 @@ EditorApp::~EditorApp()
|
||||
// Collect entities first, then delete after iteration (can't modify during iteration)
|
||||
std::vector<flecs::entity> entitiesToDelete;
|
||||
|
||||
// Destroy dialogue system before other systems
|
||||
m_dialogueSystem.reset();
|
||||
// DialogueSystem is a singleton, no manual teardown needed
|
||||
|
||||
m_startupMenuSystem.reset();
|
||||
m_playerControllerSystem.reset();
|
||||
@@ -443,8 +446,15 @@ void EditorApp::setup()
|
||||
// Setup game systems
|
||||
m_startupMenuSystem = std::make_unique<StartupMenuSystem>(
|
||||
m_world, m_sceneMgr, this);
|
||||
m_dialogueSystem = std::make_unique<DialogueSystem>(
|
||||
m_world, m_sceneMgr, this);
|
||||
DialogueSystem::getInstance().init(this);
|
||||
DialogueSystem::getInstance().loadSettings("dialogue.json");
|
||||
|
||||
m_characterClassSystem =
|
||||
std::make_unique<CharacterClassSystem>(
|
||||
m_world, this);
|
||||
CharacterClassDatabase::loadFromJson(
|
||||
"character_class.json");
|
||||
|
||||
m_playerControllerSystem =
|
||||
std::make_unique<PlayerControllerSystem>(
|
||||
m_world, m_sceneMgr, this);
|
||||
@@ -474,8 +484,7 @@ void EditorApp::setup()
|
||||
// startup_menu.json scene loaded above.
|
||||
if (m_startupMenuSystem)
|
||||
m_startupMenuSystem->prepareFont();
|
||||
if (m_dialogueSystem)
|
||||
m_dialogueSystem->prepareFont();
|
||||
DialogueSystem::getInstance().prepareFont();
|
||||
}
|
||||
|
||||
// Now show the overlay — font atlas will be built with our font
|
||||
@@ -517,6 +526,8 @@ void EditorApp::setup()
|
||||
editScene::registerLuaActionApi(L);
|
||||
editScene::registerLuaBehaviorTreeApi(L);
|
||||
editScene::registerLuaGameModeApi(L);
|
||||
editScene::registerLuaCharacterClassApi(L);
|
||||
editScene::registerLuaDialogueApi(L);
|
||||
|
||||
// Run late setup: load data.lua and initial scripts.
|
||||
m_lua.lateSetup();
|
||||
@@ -707,9 +718,10 @@ void EditorApp::setupECS()
|
||||
|
||||
// Register game components
|
||||
m_world.component<StartupMenuComponent>();
|
||||
m_world.component<DialogueComponent>();
|
||||
m_world.component<PlayerControllerComponent>();
|
||||
m_world.component<InWater>();
|
||||
m_world.component<CharacterClassComponent>();
|
||||
m_world.component<CharacterClassOverrideComponent>();
|
||||
|
||||
// Register environment components
|
||||
m_world.component<SunComponent>();
|
||||
@@ -1218,19 +1230,52 @@ flecs::entity EditorApp::getSelectedEntity() const
|
||||
return flecs::entity::null();
|
||||
}
|
||||
|
||||
DialogueSystem *EditorApp::getDialogueSystem() const
|
||||
{
|
||||
return &DialogueSystem::getInstance();
|
||||
}
|
||||
|
||||
void EditorApp::locateResources()
|
||||
{
|
||||
Ogre::ResourceGroupManager::getSingleton().createResourceGroup(
|
||||
"Characters", true);
|
||||
// Ogre::ResourceGroupManager::getSingleton().createResourceGroup(
|
||||
// "Water", true);
|
||||
Ogre::ResourceGroupManager::getSingleton().createResourceGroup(
|
||||
"LuaScripts", false);
|
||||
Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
|
||||
"./lua-scripts", "FileSystem", "LuaScripts", true, true);
|
||||
Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
|
||||
"./characters/male", "FileSystem", "Characters", false, true);
|
||||
Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
|
||||
"./characters/female", "FileSystem", "Characters", false, true);
|
||||
|
||||
/* Try multiple relative paths for characters to handle different
|
||||
* working directories (build root vs binary subdirectory) */
|
||||
struct CharPathPair {
|
||||
const char *male;
|
||||
const char *female;
|
||||
};
|
||||
CharPathPair charPaths[] = {
|
||||
{"./characters/male", "./characters/female"},
|
||||
{"../../../characters/male", "../../../characters/female"},
|
||||
{"../../characters/male", "../../characters/female"},
|
||||
{"../characters/male", "../characters/female"},
|
||||
};
|
||||
|
||||
bool added = false;
|
||||
for (const auto &pair : charPaths) {
|
||||
if (std::filesystem::exists(pair.male)) {
|
||||
Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
|
||||
pair.male, "FileSystem", "Characters", false, true);
|
||||
Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
|
||||
pair.female, "FileSystem", "Characters", false, true);
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Characters resource location added: " +
|
||||
Ogre::String(pair.male));
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!added) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"WARNING: Could not find characters directory from " +
|
||||
std::filesystem::current_path().string());
|
||||
}
|
||||
|
||||
OgreBites::ApplicationContext::locateResources();
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ class GoapPlannerSystem;
|
||||
class ActuatorSystem;
|
||||
class EventHandlerSystem;
|
||||
class ItemSystem;
|
||||
class CharacterClassSystem;
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
@@ -190,10 +191,7 @@ public:
|
||||
{
|
||||
return m_startupMenuSystem.get();
|
||||
}
|
||||
DialogueSystem *getDialogueSystem() const
|
||||
{
|
||||
return m_dialogueSystem.get();
|
||||
}
|
||||
DialogueSystem *getDialogueSystem() const;
|
||||
ActuatorSystem *getActuatorSystem() const
|
||||
{
|
||||
return m_actuatorSystem.get();
|
||||
@@ -202,6 +200,10 @@ public:
|
||||
{
|
||||
return m_eventHandlerSystem.get();
|
||||
}
|
||||
CharacterClassSystem *getCharacterClassSystem() const
|
||||
{
|
||||
return m_characterClassSystem.get();
|
||||
}
|
||||
Ogre::ImGuiOverlay *getImGuiOverlay() const
|
||||
{
|
||||
return m_imguiOverlay;
|
||||
@@ -248,10 +250,10 @@ private:
|
||||
std::unique_ptr<ActuatorSystem> m_actuatorSystem;
|
||||
std::unique_ptr<EventHandlerSystem> m_eventHandlerSystem;
|
||||
std::unique_ptr<ItemSystem> m_itemSystem;
|
||||
std::unique_ptr<CharacterClassSystem> m_characterClassSystem;
|
||||
|
||||
// Game systems
|
||||
std::unique_ptr<StartupMenuSystem> m_startupMenuSystem;
|
||||
std::unique_ptr<DialogueSystem> m_dialogueSystem;
|
||||
std::unique_ptr<PlayerControllerSystem> m_playerControllerSystem;
|
||||
|
||||
// State
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#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
|
||||
@@ -0,0 +1,442 @@
|
||||
#include "CharacterClassDatabase.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreResourceGroupManager.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Singleton
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
CharacterClassDatabase &CharacterClassDatabase::getSingleton()
|
||||
{
|
||||
static CharacterClassDatabase instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
CharacterClassDatabase *CharacterClassDatabase::getSingletonPtr()
|
||||
{
|
||||
return &getSingleton();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lookup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const CharacterClassDatabase::StatDef *
|
||||
CharacterClassDatabase::findStat(const Ogre::String &name) const
|
||||
{
|
||||
auto it = m_stats.find(name);
|
||||
if (it != m_stats.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const CharacterClassDatabase::SkillDef *
|
||||
CharacterClassDatabase::findSkill(const Ogre::String &name) const
|
||||
{
|
||||
auto it = m_skills.find(name);
|
||||
if (it != m_skills.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const CharacterClassDatabase::NeedDef *
|
||||
CharacterClassDatabase::findNeed(const Ogre::String &name) const
|
||||
{
|
||||
auto it = m_needs.find(name);
|
||||
if (it != m_needs.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const CharacterClassDatabase::ClassDef *
|
||||
CharacterClassDatabase::findClass(const Ogre::String &name) const
|
||||
{
|
||||
auto it = m_classes.find(name);
|
||||
if (it != m_classes.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CharacterClassDatabase::StatDef *
|
||||
CharacterClassDatabase::findStat(const Ogre::String &name)
|
||||
{
|
||||
auto it = m_stats.find(name);
|
||||
if (it != m_stats.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CharacterClassDatabase::SkillDef *
|
||||
CharacterClassDatabase::findSkill(const Ogre::String &name)
|
||||
{
|
||||
auto it = m_skills.find(name);
|
||||
if (it != m_skills.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CharacterClassDatabase::NeedDef *
|
||||
CharacterClassDatabase::findNeed(const Ogre::String &name)
|
||||
{
|
||||
auto it = m_needs.find(name);
|
||||
if (it != m_needs.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CharacterClassDatabase::ClassDef *
|
||||
CharacterClassDatabase::findClass(const Ogre::String &name)
|
||||
{
|
||||
auto it = m_classes.find(name);
|
||||
if (it != m_classes.end())
|
||||
return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutators
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CharacterClassDatabase::addOrReplaceStat(const StatDef &def)
|
||||
{
|
||||
bool wasNew = m_stats.find(def.name) == m_stats.end();
|
||||
m_stats[def.name] = def;
|
||||
if (wasNew)
|
||||
m_statNames.push_back(def.name);
|
||||
}
|
||||
|
||||
void CharacterClassDatabase::addOrReplaceSkill(const SkillDef &def)
|
||||
{
|
||||
bool wasNew = m_skills.find(def.name) == m_skills.end();
|
||||
m_skills[def.name] = def;
|
||||
if (wasNew)
|
||||
m_skillNames.push_back(def.name);
|
||||
}
|
||||
|
||||
void CharacterClassDatabase::addOrReplaceNeed(const NeedDef &def)
|
||||
{
|
||||
bool wasNew = m_needs.find(def.name) == m_needs.end();
|
||||
m_needs[def.name] = def;
|
||||
if (wasNew)
|
||||
m_needNames.push_back(def.name);
|
||||
}
|
||||
|
||||
void CharacterClassDatabase::addOrReplaceClass(const ClassDef &def)
|
||||
{
|
||||
bool wasNew = m_classes.find(def.name) == m_classes.end();
|
||||
m_classes[def.name] = def;
|
||||
if (wasNew)
|
||||
m_classNames.push_back(def.name);
|
||||
}
|
||||
|
||||
bool CharacterClassDatabase::removeStat(const Ogre::String &name)
|
||||
{
|
||||
if (m_stats.erase(name) == 0)
|
||||
return false;
|
||||
auto it = std::remove(m_statNames.begin(), m_statNames.end(),
|
||||
name);
|
||||
m_statNames.erase(it, m_statNames.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CharacterClassDatabase::removeSkill(const Ogre::String &name)
|
||||
{
|
||||
if (m_skills.erase(name) == 0)
|
||||
return false;
|
||||
auto it = std::remove(m_skillNames.begin(), m_skillNames.end(),
|
||||
name);
|
||||
m_skillNames.erase(it, m_skillNames.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CharacterClassDatabase::removeNeed(const Ogre::String &name)
|
||||
{
|
||||
if (m_needs.erase(name) == 0)
|
||||
return false;
|
||||
auto it = std::remove(m_needNames.begin(), m_needNames.end(),
|
||||
name);
|
||||
m_needNames.erase(it, m_needNames.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CharacterClassDatabase::removeClass(const Ogre::String &name)
|
||||
{
|
||||
if (m_classes.erase(name) == 0)
|
||||
return false;
|
||||
auto it = std::remove(m_classNames.begin(), m_classNames.end(),
|
||||
name);
|
||||
m_classNames.erase(it, m_classNames.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
void CharacterClassDatabase::clear()
|
||||
{
|
||||
m_stats.clear();
|
||||
m_skills.clear();
|
||||
m_needs.clear();
|
||||
m_classes.clear();
|
||||
m_statNames.clear();
|
||||
m_skillNames.clear();
|
||||
m_needNames.clear();
|
||||
m_classNames.clear();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Runtime helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int64_t CharacterClassDatabase::computeXPForLevel(int level,
|
||||
const ClassDef &cls) const
|
||||
{
|
||||
return static_cast<int64_t>(cls.xpForLevel.evaluate(level));
|
||||
}
|
||||
|
||||
int CharacterClassDatabase::computePointsForLevel(int level,
|
||||
const ClassDef &cls) const
|
||||
{
|
||||
return static_cast<int>(cls.pointsPerLevel.evaluate(level));
|
||||
}
|
||||
|
||||
int CharacterClassDatabase::computeStatGrowth(const Ogre::String &stat,
|
||||
int level,
|
||||
const ClassDef &cls) const
|
||||
{
|
||||
auto it = cls.statGrowth.find(stat);
|
||||
if (it == cls.statGrowth.end())
|
||||
return 0;
|
||||
return static_cast<int>(it->second.evaluate(level));
|
||||
}
|
||||
|
||||
int CharacterClassDatabase::computeSkillGrowth(const Ogre::String &skill,
|
||||
int level,
|
||||
const ClassDef &cls) const
|
||||
{
|
||||
auto it = cls.skillGrowth.find(skill);
|
||||
if (it == cls.skillGrowth.end())
|
||||
return 0;
|
||||
return static_cast<int>(it->second.evaluate(level));
|
||||
}
|
||||
|
||||
int CharacterClassDatabase::computeStatCost(int currentValue,
|
||||
const ClassDef &cls) const
|
||||
{
|
||||
return static_cast<int>(cls.statCost.evaluate(0, static_cast<double>(currentValue)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON serialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool CharacterClassDatabase::saveToJson(const std::string &filename)
|
||||
{
|
||||
auto &db = getSingleton();
|
||||
nlohmann::json json;
|
||||
json["version"] = 1;
|
||||
|
||||
// Stats
|
||||
json["stat_definitions"] = nlohmann::json::object();
|
||||
for (const auto &name : db.m_statNames) {
|
||||
const auto &def = db.m_stats.at(name);
|
||||
nlohmann::json j;
|
||||
j["display_name"] = def.displayName;
|
||||
j["kind"] = (def.kind == StatKind::Attribute) ? "attribute" :
|
||||
"resource_pool";
|
||||
j["min"] = def.minValue;
|
||||
j["max"] = def.maxValue;
|
||||
json["stat_definitions"][name] = j;
|
||||
}
|
||||
|
||||
// Skills
|
||||
json["skill_definitions"] = nlohmann::json::object();
|
||||
for (const auto &name : db.m_skillNames) {
|
||||
const auto &def = db.m_skills.at(name);
|
||||
nlohmann::json j;
|
||||
j["display_name"] = def.displayName;
|
||||
j["max"] = def.maxValue;
|
||||
json["skill_definitions"][name] = j;
|
||||
}
|
||||
|
||||
// Needs
|
||||
json["need_definitions"] = nlohmann::json::object();
|
||||
for (const auto &name : db.m_needNames) {
|
||||
const auto &def = db.m_needs.at(name);
|
||||
nlohmann::json j;
|
||||
j["display_name"] = def.displayName;
|
||||
j["max"] = def.maxValue;
|
||||
j["accumulation_rate"] = def.accumulationRate;
|
||||
j["low_threshold"] = def.lowThreshold;
|
||||
j["high_threshold"] = def.highThreshold;
|
||||
j["bit_name"] = def.bitName;
|
||||
json["need_definitions"][name] = j;
|
||||
}
|
||||
|
||||
// Classes
|
||||
json["classes"] = nlohmann::json::object();
|
||||
for (const auto &name : db.m_classNames) {
|
||||
const auto &cls = db.m_classes.at(name);
|
||||
nlohmann::json j;
|
||||
j["description"] = cls.description;
|
||||
j["primary_stats"] = cls.primaryStats;
|
||||
j["base_stats"] = cls.baseStats;
|
||||
j["base_skills"] = cls.baseSkills;
|
||||
j["base_needs"] = cls.baseNeeds;
|
||||
j["xp_for_level"] = cls.xpForLevel.getExpression();
|
||||
j["points_per_level"] = cls.pointsPerLevel.getExpression();
|
||||
j["stat_cost"] = cls.statCost.getExpression();
|
||||
j["stat_growth"] = nlohmann::json::object();
|
||||
for (const auto &pair : cls.statGrowth)
|
||||
j["stat_growth"][pair.first] =
|
||||
pair.second.getExpression();
|
||||
j["skill_growth"] = nlohmann::json::object();
|
||||
for (const auto &pair : cls.skillGrowth)
|
||||
j["skill_growth"][pair.first] =
|
||||
pair.second.getExpression();
|
||||
json["classes"][name] = j;
|
||||
}
|
||||
|
||||
try {
|
||||
std::filesystem::path outPath =
|
||||
std::filesystem::current_path() / filename;
|
||||
std::ofstream f(outPath);
|
||||
if (!f.is_open()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CharacterClassDatabase: Failed to open " +
|
||||
filename + " for writing");
|
||||
return false;
|
||||
}
|
||||
f << json.dump(4);
|
||||
return true;
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CharacterClassDatabase: Exception saving " + filename);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool CharacterClassDatabase::loadFromJson(const std::string &filename)
|
||||
{
|
||||
auto &db = getSingleton();
|
||||
db.clear();
|
||||
|
||||
std::filesystem::path inPath =
|
||||
std::filesystem::current_path() / filename;
|
||||
std::ifstream f(inPath);
|
||||
if (!f.is_open()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CharacterClassDatabase: Could not load " + filename +
|
||||
", using defaults.");
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json json;
|
||||
try {
|
||||
f >> json;
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CharacterClassDatabase: JSON parse error in " +
|
||||
filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stats
|
||||
if (json.contains("stat_definitions") &&
|
||||
json["stat_definitions"].is_object()) {
|
||||
for (auto &[key, val] : json["stat_definitions"].items()) {
|
||||
StatDef def;
|
||||
def.name = key;
|
||||
def.displayName = val.value("display_name", key);
|
||||
Ogre::String kindStr = val.value("kind", "attribute");
|
||||
def.kind = (kindStr == "resource_pool") ? StatKind::ResourcePool :
|
||||
StatKind::Attribute;
|
||||
def.minValue = val.value("min", 1);
|
||||
def.maxValue = val.value("max", 999);
|
||||
db.addOrReplaceStat(def);
|
||||
}
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (json.contains("skill_definitions") &&
|
||||
json["skill_definitions"].is_object()) {
|
||||
for (auto &[key, val] : json["skill_definitions"].items()) {
|
||||
SkillDef def;
|
||||
def.name = key;
|
||||
def.displayName = val.value("display_name", key);
|
||||
def.maxValue = val.value("max", 100);
|
||||
db.addOrReplaceSkill(def);
|
||||
}
|
||||
}
|
||||
|
||||
// Needs
|
||||
if (json.contains("need_definitions") &&
|
||||
json["need_definitions"].is_object()) {
|
||||
for (auto &[key, val] : json["need_definitions"].items()) {
|
||||
NeedDef def;
|
||||
def.name = key;
|
||||
def.displayName = val.value("display_name", key);
|
||||
def.maxValue = val.value("max", 1000);
|
||||
def.accumulationRate = val.value("accumulation_rate", 0.0f);
|
||||
def.lowThreshold = val.value("low_threshold", 0);
|
||||
def.highThreshold = val.value("high_threshold", 1000);
|
||||
def.bitName = val.value("bit_name", "");
|
||||
// Backward compat: old files used low_bit_name / high_bit_name
|
||||
if (def.bitName.empty()) {
|
||||
def.bitName = val.value("high_bit_name", "");
|
||||
if (def.bitName.empty())
|
||||
def.bitName = val.value("low_bit_name", "");
|
||||
}
|
||||
db.addOrReplaceNeed(def);
|
||||
}
|
||||
}
|
||||
|
||||
// Classes
|
||||
if (json.contains("classes") && json["classes"].is_object()) {
|
||||
for (auto &[key, val] : json["classes"].items()) {
|
||||
ClassDef cls;
|
||||
cls.name = key;
|
||||
cls.description = val.value("description", "");
|
||||
if (val.contains("primary_stats") &&
|
||||
val["primary_stats"].is_array()) {
|
||||
for (const auto &item : val["primary_stats"])
|
||||
cls.primaryStats.push_back(
|
||||
item.get<std::string>());
|
||||
}
|
||||
if (val.contains("base_stats") &&
|
||||
val["base_stats"].is_object()) {
|
||||
for (auto &[k, v] : val["base_stats"].items())
|
||||
cls.baseStats[k] = v.get<int>();
|
||||
}
|
||||
if (val.contains("base_skills") &&
|
||||
val["base_skills"].is_object()) {
|
||||
for (auto &[k, v] : val["base_skills"].items())
|
||||
cls.baseSkills[k] = v.get<int>();
|
||||
}
|
||||
if (val.contains("base_needs") &&
|
||||
val["base_needs"].is_object()) {
|
||||
for (auto &[k, v] : val["base_needs"].items())
|
||||
cls.baseNeeds[k] = v.get<int>();
|
||||
}
|
||||
cls.xpForLevel = Formula(val.value("xp_for_level", "0"));
|
||||
cls.pointsPerLevel =
|
||||
Formula(val.value("points_per_level", "0"));
|
||||
cls.statCost = Formula(val.value("stat_cost", "1"));
|
||||
if (val.contains("stat_growth") &&
|
||||
val["stat_growth"].is_object()) {
|
||||
for (auto &[k, v] : val["stat_growth"].items())
|
||||
cls.statGrowth[k] =
|
||||
Formula(v.get<std::string>());
|
||||
}
|
||||
if (val.contains("skill_growth") &&
|
||||
val["skill_growth"].is_object()) {
|
||||
for (auto &[k, v] : val["skill_growth"].items())
|
||||
cls.skillGrowth[k] =
|
||||
Formula(v.get<std::string>());
|
||||
}
|
||||
db.addOrReplaceClass(cls);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
#ifndef EDITSCENE_CHARACTER_CLASS_DATABASE_HPP
|
||||
#define EDITSCENE_CHARACTER_CLASS_DATABASE_HPP
|
||||
#pragma once
|
||||
|
||||
#include "Formula.hpp"
|
||||
#include <Ogre.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Global character class database singleton.
|
||||
*
|
||||
* Holds the master list of stat/skill/need definitions and class templates.
|
||||
* This is a singleton accessible from anywhere in the codebase.
|
||||
* Persisted to character_class.json, read-only in game mode.
|
||||
*/
|
||||
class CharacterClassDatabase {
|
||||
public:
|
||||
static CharacterClassDatabase &getSingleton();
|
||||
static CharacterClassDatabase *getSingletonPtr();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Definitions
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
enum class StatKind {
|
||||
Attribute, // permanent value (strength, agility)
|
||||
ResourcePool // depletable pool (health, stamina, mana)
|
||||
};
|
||||
|
||||
struct StatDef {
|
||||
Ogre::String name;
|
||||
Ogre::String displayName;
|
||||
StatKind kind = StatKind::Attribute;
|
||||
int minValue = 1;
|
||||
int maxValue = 999;
|
||||
};
|
||||
|
||||
struct SkillDef {
|
||||
Ogre::String name;
|
||||
Ogre::String displayName;
|
||||
int maxValue = 100;
|
||||
};
|
||||
|
||||
struct NeedDef {
|
||||
Ogre::String name;
|
||||
Ogre::String displayName;
|
||||
int maxValue = 1000;
|
||||
float accumulationRate = 0.0f;
|
||||
int lowThreshold = 0; // clear bit when need <= this
|
||||
int highThreshold = 1000; // set bit when need >= this
|
||||
Ogre::String bitName; // GOAP blackboard bit (hysteresis)
|
||||
};
|
||||
|
||||
struct ClassDef {
|
||||
Ogre::String name;
|
||||
Ogre::String description;
|
||||
std::vector<Ogre::String> primaryStats;
|
||||
std::unordered_map<Ogre::String, int> baseStats;
|
||||
std::unordered_map<Ogre::String, int> baseSkills;
|
||||
std::unordered_map<Ogre::String, int> baseNeeds;
|
||||
Formula xpForLevel;
|
||||
Formula pointsPerLevel;
|
||||
Formula statCost;
|
||||
std::unordered_map<Ogre::String, Formula> statGrowth;
|
||||
std::unordered_map<Ogre::String, Formula> skillGrowth;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Lookup
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
const StatDef *findStat(const Ogre::String &name) const;
|
||||
const SkillDef *findSkill(const Ogre::String &name) const;
|
||||
const NeedDef *findNeed(const Ogre::String &name) const;
|
||||
const ClassDef *findClass(const Ogre::String &name) const;
|
||||
|
||||
StatDef *findStat(const Ogre::String &name);
|
||||
SkillDef *findSkill(const Ogre::String &name);
|
||||
NeedDef *findNeed(const Ogre::String &name);
|
||||
ClassDef *findClass(const Ogre::String &name);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Mutators
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
void addOrReplaceStat(const StatDef &def);
|
||||
void addOrReplaceSkill(const SkillDef &def);
|
||||
void addOrReplaceNeed(const NeedDef &def);
|
||||
void addOrReplaceClass(const ClassDef &def);
|
||||
|
||||
bool removeStat(const Ogre::String &name);
|
||||
bool removeSkill(const Ogre::String &name);
|
||||
bool removeNeed(const Ogre::String &name);
|
||||
bool removeClass(const Ogre::String &name);
|
||||
|
||||
void clear();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Lists
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
const std::vector<Ogre::String> &getStatNames() const
|
||||
{
|
||||
return m_statNames;
|
||||
}
|
||||
const std::vector<Ogre::String> &getSkillNames() const
|
||||
{
|
||||
return m_skillNames;
|
||||
}
|
||||
const std::vector<Ogre::String> &getNeedNames() const
|
||||
{
|
||||
return m_needNames;
|
||||
}
|
||||
const std::vector<Ogre::String> &getClassNames() const
|
||||
{
|
||||
return m_classNames;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Persistence
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static bool saveToJson(const std::string &filename);
|
||||
static bool loadFromJson(const std::string &filename);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Runtime helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
int64_t computeXPForLevel(int level, const ClassDef &cls) const;
|
||||
int computePointsForLevel(int level, const ClassDef &cls) const;
|
||||
int computeStatGrowth(const Ogre::String &stat, int level,
|
||||
const ClassDef &cls) const;
|
||||
int computeSkillGrowth(const Ogre::String &skill, int level,
|
||||
const ClassDef &cls) const;
|
||||
int computeStatCost(int currentValue, const ClassDef &cls) const;
|
||||
|
||||
private:
|
||||
CharacterClassDatabase() = default;
|
||||
|
||||
std::unordered_map<Ogre::String, StatDef> m_stats;
|
||||
std::unordered_map<Ogre::String, SkillDef> m_skills;
|
||||
std::unordered_map<Ogre::String, NeedDef> m_needs;
|
||||
std::unordered_map<Ogre::String, ClassDef> m_classes;
|
||||
|
||||
std::vector<Ogre::String> m_statNames;
|
||||
std::vector<Ogre::String> m_skillNames;
|
||||
std::vector<Ogre::String> m_needNames;
|
||||
std::vector<Ogre::String> m_classNames;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTER_CLASS_DATABASE_HPP
|
||||
@@ -0,0 +1,22 @@
|
||||
#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>();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#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>();
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,18 @@
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Selection criteria for a single character slot.
|
||||
* Layer 0 (nude base) is always implicit.
|
||||
* Layer 1 and 2 are selected via combo boxes.
|
||||
*/
|
||||
struct SlotSelection {
|
||||
Ogre::String layer1Mesh; // "none" or exact mesh name for layer 1
|
||||
Ogre::String layer2Mesh; // "none" or exact mesh name for layer 2
|
||||
Ogre::String explicitMesh; // backward-compat override
|
||||
};
|
||||
|
||||
/**
|
||||
* Multi-slot mesh component for character parts sharing a skeleton.
|
||||
@@ -11,7 +23,16 @@
|
||||
struct CharacterSlotsComponent {
|
||||
Ogre::String age = "adult";
|
||||
Ogre::String sex = "male";
|
||||
|
||||
/* Global outfit level: 0=nude, 1=lingerie, 2=clothed */
|
||||
int outfitLevel = 2;
|
||||
|
||||
/* Backward-compat: old mesh-name map. Deserialized into slotSelections on load. */
|
||||
std::unordered_map<Ogre::String, Ogre::String> slots;
|
||||
|
||||
/* Per-slot layer selections (runtime) */
|
||||
std::unordered_map<Ogre::String, SlotSelection> slotSelections;
|
||||
|
||||
bool dirty = true;
|
||||
|
||||
/* Runtime: master entity with shared skeleton (set by CharacterSlotSystem) */
|
||||
@@ -20,11 +41,19 @@ struct CharacterSlotsComponent {
|
||||
/**
|
||||
* Front-facing axis for this character model.
|
||||
* Most models face -Z (NEGATIVE_UNIT_Z), but some face +Z.
|
||||
* This is used by path following to rotate the character correctly.
|
||||
*/
|
||||
Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
|
||||
CharacterSlotsComponent() = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global shape key weights for a character.
|
||||
* Same name applies to all slots uniformly.
|
||||
*/
|
||||
struct CharacterShapeKeysComponent {
|
||||
std::unordered_map<Ogre::String, float> weights;
|
||||
bool dirty = true;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTERSLOTS_HPP
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
#ifndef EDITSCENE_DIALOGUE_COMPONENT_HPP
|
||||
#define EDITSCENE_DIALOGUE_COMPONENT_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Visual-novel style dialogue box component.
|
||||
*
|
||||
* Displays a narration text box at the bottom of the screen with optional
|
||||
* player choices. The dialogue can be driven via the EventBus system
|
||||
* (using "dialogue_show" event) or directly via the component API.
|
||||
*
|
||||
* Only active in game mode (GamePlayState::Playing).
|
||||
*
|
||||
* Event payload (EventParams) parameters:
|
||||
* "text" (string) - Narration text to display
|
||||
* "choices" (string_array) - Array of choice label strings (Lua table)
|
||||
* "speaker" (string) - Optional speaker name
|
||||
* "auto_progress" (int) - If 1, clicking anywhere progresses (no choices)
|
||||
*
|
||||
* Component state transitions:
|
||||
* Idle -> Showing (on show() or event)
|
||||
* Showing -> AwaitingChoice (if choices provided)
|
||||
* Showing -> Idle (if no choices, on click progress)
|
||||
* AwaitingChoice -> Idle (on choice selected)
|
||||
*/
|
||||
struct DialogueComponent {
|
||||
/** Current state of the dialogue box */
|
||||
enum class State {
|
||||
Idle, ///< No dialogue active
|
||||
Showing, ///< Text is being displayed
|
||||
AwaitingChoice ///< Waiting for player to pick a choice
|
||||
};
|
||||
|
||||
State state = State::Idle;
|
||||
|
||||
/** The narration text to display */
|
||||
Ogre::String text;
|
||||
|
||||
/** Optional speaker name (displayed above the text) */
|
||||
Ogre::String speaker;
|
||||
|
||||
/** Player choice labels (empty = no choices, click to progress) */
|
||||
std::vector<Ogre::String> choices;
|
||||
|
||||
/** Font configuration */
|
||||
Ogre::String fontName = "Jupiteroid-Regular.ttf";
|
||||
float fontSize = 24.0f;
|
||||
|
||||
/** Speaker name font size (slightly smaller) */
|
||||
float speakerFontSize = 20.0f;
|
||||
|
||||
/** Background opacity (0.0 - 1.0) */
|
||||
float backgroundOpacity = 0.85f;
|
||||
|
||||
/** Height of the dialogue box as fraction of screen height (0.0 - 1.0) */
|
||||
float boxHeightFraction = 0.25f;
|
||||
|
||||
/** Vertical position as fraction from top (0.0 = top, 0.75 = bottom quarter) */
|
||||
float boxPositionFraction = 0.75f;
|
||||
|
||||
/** Whether the dialogue box is enabled (can be toggled) */
|
||||
bool enabled = true;
|
||||
|
||||
/** Callback invoked when a choice is selected (choice index, 1-based) */
|
||||
std::function<void(int)> onChoiceSelected;
|
||||
|
||||
/** Callback invoked when dialogue is dismissed (no choices mode) */
|
||||
std::function<void()> onDismissed;
|
||||
|
||||
/** Callback invoked when dialogue starts showing */
|
||||
std::function<void()> onShow;
|
||||
|
||||
/* --- API --- */
|
||||
|
||||
/** Show dialogue with given text and optional choices */
|
||||
void show(const Ogre::String &narrationText,
|
||||
const std::vector<Ogre::String> &choiceLabels = {},
|
||||
const Ogre::String &speakerName = "")
|
||||
{
|
||||
text = narrationText;
|
||||
choices = choiceLabels;
|
||||
speaker = speakerName;
|
||||
state = choices.empty() ? State::Showing :
|
||||
State::AwaitingChoice;
|
||||
if (onShow)
|
||||
onShow();
|
||||
}
|
||||
|
||||
/** Progress the dialogue (click-through when no choices) */
|
||||
void progress()
|
||||
{
|
||||
if (state == State::Showing && choices.empty()) {
|
||||
state = State::Idle;
|
||||
if (onDismissed)
|
||||
onDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
/** Select a choice by 1-based index */
|
||||
void selectChoice(int index)
|
||||
{
|
||||
if (state == State::AwaitingChoice && index >= 1 &&
|
||||
index <= (int)choices.size()) {
|
||||
state = State::Idle;
|
||||
if (onChoiceSelected)
|
||||
onChoiceSelected(index);
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if dialogue is currently active */
|
||||
bool isActive() const
|
||||
{
|
||||
return state != State::Idle;
|
||||
}
|
||||
|
||||
/** Reset dialogue to idle state */
|
||||
void reset()
|
||||
{
|
||||
state = State::Idle;
|
||||
text.clear();
|
||||
choices.clear();
|
||||
speaker.clear();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DIALOGUE_COMPONENT_HPP
|
||||
@@ -1,23 +0,0 @@
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/DialogueEditor.hpp"
|
||||
#include "DialogueComponent.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Dialogue Box", "Game", DialogueComponent,
|
||||
DialogueEditor)
|
||||
{
|
||||
registry.registerComponent<DialogueComponent>(
|
||||
DialogueComponent_name, DialogueComponent_group,
|
||||
std::make_unique<DialogueEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<DialogueComponent>()) {
|
||||
e.set<DialogueComponent>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<DialogueComponent>()) {
|
||||
e.remove<DialogueComponent>();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
#include "Formula.hpp"
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <OgreLogManager.h>
|
||||
|
||||
Formula::Formula(const Ogre::String &expr)
|
||||
: m_expression(expr)
|
||||
{
|
||||
m_valid = !expr.empty();
|
||||
}
|
||||
|
||||
double Formula::evaluate(const std::unordered_map<std::string, double> &vars) const
|
||||
{
|
||||
if (!m_valid || m_expression.empty())
|
||||
return 0.0;
|
||||
|
||||
Parser p;
|
||||
p.s = m_expression.c_str();
|
||||
p.vars = &vars;
|
||||
try {
|
||||
return p.parseExpression();
|
||||
} catch (...) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
double Formula::evaluate(int level) const
|
||||
{
|
||||
std::unordered_map<std::string, double> vars;
|
||||
vars["level"] = static_cast<double>(level);
|
||||
return evaluate(vars);
|
||||
}
|
||||
|
||||
double Formula::evaluate(int level, double current) const
|
||||
{
|
||||
std::unordered_map<std::string, double> vars;
|
||||
vars["level"] = static_cast<double>(level);
|
||||
vars["current"] = current;
|
||||
return evaluate(vars);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parser implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void Formula::Parser::skipWhitespace()
|
||||
{
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
}
|
||||
|
||||
bool Formula::Parser::consume(char c)
|
||||
{
|
||||
skipWhitespace();
|
||||
if (*s == c) {
|
||||
s++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
double Formula::Parser::parseExpression()
|
||||
{
|
||||
double value = parseTerm();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (consume('+')) {
|
||||
value += parseTerm();
|
||||
} else if (consume('-')) {
|
||||
value -= parseTerm();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
double Formula::Parser::parseTerm()
|
||||
{
|
||||
double value = parsePower();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (consume('*')) {
|
||||
value *= parsePower();
|
||||
} else if (consume('/')) {
|
||||
double rhs = parsePower();
|
||||
if (rhs != 0.0)
|
||||
value /= rhs;
|
||||
else
|
||||
value = 0.0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
double Formula::Parser::parsePower()
|
||||
{
|
||||
double value = parseUnary();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (consume('^')) {
|
||||
value = std::pow(value, parseUnary());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
double Formula::Parser::parseUnary()
|
||||
{
|
||||
skipWhitespace();
|
||||
if (consume('+'))
|
||||
return parseUnary();
|
||||
if (consume('-'))
|
||||
return -parseUnary();
|
||||
return parsePrimary();
|
||||
}
|
||||
|
||||
double Formula::Parser::parsePrimary()
|
||||
{
|
||||
skipWhitespace();
|
||||
|
||||
// Number
|
||||
if (std::isdigit(*s) || (*s == '.' && std::isdigit(*(s + 1)))) {
|
||||
char *end = nullptr;
|
||||
double val = std::strtod(s, &end);
|
||||
s = end;
|
||||
return val;
|
||||
}
|
||||
|
||||
// Parenthesized expression
|
||||
if (consume('(')) {
|
||||
double val = parseExpression();
|
||||
skipWhitespace();
|
||||
if (!consume(')'))
|
||||
return 0.0;
|
||||
return val;
|
||||
}
|
||||
|
||||
// Identifier (variable or function)
|
||||
if (std::isalpha(*s) || *s == '_') {
|
||||
const char *start = s;
|
||||
while (std::isalnum(*s) || *s == '_')
|
||||
s++;
|
||||
std::string name(start, static_cast<size_t>(s - start));
|
||||
|
||||
skipWhitespace();
|
||||
if (consume('(')) {
|
||||
// Function call
|
||||
std::vector<double> args;
|
||||
if (!consume(')')) {
|
||||
args.push_back(parseExpression());
|
||||
while (consume(',')) {
|
||||
args.push_back(parseExpression());
|
||||
}
|
||||
skipWhitespace();
|
||||
if (!consume(')'))
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if (name == "floor" && args.size() >= 1)
|
||||
return std::floor(args[0]);
|
||||
if (name == "ceil" && args.size() >= 1)
|
||||
return std::ceil(args[0]);
|
||||
if (name == "min" && args.size() >= 2)
|
||||
return args[0] < args[1] ? args[0] : args[1];
|
||||
if (name == "max" && args.size() >= 2)
|
||||
return args[0] > args[1] ? args[0] : args[1];
|
||||
if (name == "clamp" && args.size() >= 3) {
|
||||
if (args[0] < args[1])
|
||||
return args[1];
|
||||
if (args[0] > args[2])
|
||||
return args[2];
|
||||
return args[0];
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Variable lookup
|
||||
if (vars) {
|
||||
auto it = vars->find(name);
|
||||
if (it != vars->end())
|
||||
return it->second;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifndef EDITSCENE_FORMULA_HPP
|
||||
#define EDITSCENE_FORMULA_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* Lightweight expression evaluator for RPG formulas.
|
||||
*
|
||||
* Supports:
|
||||
* Variables: level, current, base, value
|
||||
* Operators: +, -, *, /, ^, ()
|
||||
* Functions: floor(x), ceil(x), min(a,b), max(a,b), clamp(x,lo,hi)
|
||||
*
|
||||
* Example: "level * level * 100 + floor(current / 10)"
|
||||
*/
|
||||
class Formula {
|
||||
public:
|
||||
Formula() = default;
|
||||
explicit Formula(const Ogre::String &expr);
|
||||
|
||||
bool isValid() const { return m_valid; }
|
||||
const Ogre::String &getExpression() const { return m_expression; }
|
||||
|
||||
/**
|
||||
* Evaluate the formula with given variable bindings.
|
||||
*
|
||||
* @param vars Map of variable names to values.
|
||||
* @return The computed result. Returns 0.0 if formula is invalid.
|
||||
*/
|
||||
double evaluate(const std::unordered_map<std::string, double> &vars) const;
|
||||
|
||||
/**
|
||||
* Convenience: evaluate with just a level.
|
||||
*/
|
||||
double evaluate(int level) const;
|
||||
|
||||
/**
|
||||
* Convenience: evaluate with level and current value.
|
||||
*/
|
||||
double evaluate(int level, double current) const;
|
||||
|
||||
private:
|
||||
Ogre::String m_expression;
|
||||
bool m_valid = false;
|
||||
|
||||
// Recursive descent parser
|
||||
struct Parser {
|
||||
const char *s;
|
||||
const std::unordered_map<std::string, double> *vars;
|
||||
|
||||
double parseExpression();
|
||||
double parseTerm();
|
||||
double parsePower();
|
||||
double parseUnary();
|
||||
double parsePrimary();
|
||||
|
||||
void skipWhitespace();
|
||||
bool consume(char c);
|
||||
};
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_FORMULA_HPP
|
||||
@@ -4,8 +4,8 @@
|
||||
-- This example demonstrates how to show a simple dialogue box using the
|
||||
-- EventBus "dialogue_show" event.
|
||||
--
|
||||
-- The DialogueSystem listens for "dialogue_show" events and displays the
|
||||
-- text on any entity that has a DialogueComponent.
|
||||
-- The DialogueSystem is a singleton (no ECS component needed). It listens
|
||||
-- for "dialogue_show" events and displays the text directly.
|
||||
--
|
||||
-- Event payload parameters:
|
||||
-- "text" (string) - Narration text to display
|
||||
@@ -14,32 +14,7 @@
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Create an entity with a DialogueComponent
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- First we need an entity that has the Dialogue component so the system
|
||||
-- knows where to render the dialogue box.
|
||||
|
||||
local dialogue_entity = ecs.create_entity()
|
||||
ecs.set_entity_name(dialogue_entity, "DialogueBox")
|
||||
|
||||
-- Add the Dialogue component with default settings:
|
||||
ecs.add_component(dialogue_entity, "Dialogue")
|
||||
|
||||
-- You can also configure the dialogue box appearance:
|
||||
ecs.set_component(dialogue_entity, "Dialogue", {
|
||||
fontName = "Jupiteroid-Regular.ttf",
|
||||
fontSize = 24.0,
|
||||
speakerFontSize = 20.0,
|
||||
backgroundOpacity = 0.85,
|
||||
boxHeightFraction = 0.25, -- 25% of screen height
|
||||
boxPositionFraction = 0.75, -- bottom quarter of screen
|
||||
enabled = true
|
||||
})
|
||||
|
||||
print("Dialogue entity created with ID: " .. dialogue_entity)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Show a simple narration (no choices)
|
||||
-- 1. Show a simple narration (no choices)
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Send a "dialogue_show" event with just text. The dialogue box will appear
|
||||
-- and the player can click anywhere to dismiss it.
|
||||
@@ -52,7 +27,7 @@ ecs.send_event("dialogue_show", {
|
||||
print("Sent basic narration dialogue")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Show dialogue with player choices
|
||||
-- 2. Show dialogue with player choices
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- When "choices" is provided as a table, the dialogue box shows
|
||||
-- buttons instead of click-to-progress. The player must pick one.
|
||||
@@ -66,7 +41,7 @@ ecs.send_event("dialogue_show", {
|
||||
print("Sent dialogue with choices")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Show dialogue without a speaker name
|
||||
-- 3. Show dialogue without a speaker name
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ecs.send_event("dialogue_show", {
|
||||
@@ -76,7 +51,7 @@ ecs.send_event("dialogue_show", {
|
||||
print("Sent anonymous narration")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Multi-line dialogue (use \n for line breaks)
|
||||
-- 4. Multi-line dialogue (use \n for line breaks)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ecs.send_event("dialogue_show", {
|
||||
@@ -90,13 +65,11 @@ print("Sent multi-line dialogue")
|
||||
-- Summary
|
||||
-- =============================================================================
|
||||
-- To show dialogue from Lua:
|
||||
-- 1. Ensure an entity with DialogueComponent exists (create one if needed)
|
||||
-- 2. Call ecs.send_event("dialogue_show", { text = "...", speaker = "...", choices = { ... } })
|
||||
-- 3. Required: text = "The narration text"
|
||||
-- 4. Optional: speaker = "Speaker Name"
|
||||
-- 5. Optional: choices = { "Choice1", "Choice2", "Choice3" } (table of strings)
|
||||
-- 6. EventParams uses flat key-value pairs (no nested stringValues/floatValues/etc.)
|
||||
-- 7. Type metadata is available via params._types table
|
||||
-- 1. Call ecs.send_event("dialogue_show", { text = "...", speaker = "...", choices = { ... } })
|
||||
-- 2. Required: text = "The narration text"
|
||||
-- 3. Optional: speaker = "Speaker Name"
|
||||
-- 4. Optional: choices = { "Choice1", "Choice2", "Choice3" } (table of strings)
|
||||
-- 5. No ECS entity or component needed — DialogueSystem is a singleton
|
||||
-- =============================================================================
|
||||
|
||||
print("Dialogue basic show examples completed!")
|
||||
|
||||
@@ -1,219 +1,198 @@
|
||||
-- =============================================================================
|
||||
-- Dialogue: Direct Component API Control
|
||||
-- Dialogue: Direct Singleton API Control
|
||||
-- =============================================================================
|
||||
-- This example demonstrates how to control the DialogueComponent directly
|
||||
-- via the ECS component API, without using the EventBus.
|
||||
-- This example demonstrates how to control the DialogueSystem directly
|
||||
-- via its singleton Lua API, without using the EventBus.
|
||||
--
|
||||
-- The DialogueComponent has methods that can be called from C++:
|
||||
-- The DialogueSystem singleton exposes these functions:
|
||||
-- show(text, choices, speaker) - Display dialogue
|
||||
-- progress() - Dismiss (no-choices mode)
|
||||
-- selectChoice(index) - Select a choice (1-based)
|
||||
-- isActive() - Check if dialogue is active
|
||||
-- reset() - Reset to idle state
|
||||
-- hide() - Dismiss dialogue
|
||||
-- progress() - Click-to-progress (no-choices mode)
|
||||
-- select_choice(index) - Select a choice (1-based)
|
||||
-- is_active() - Check if dialogue is active
|
||||
-- get_settings() - Get visual settings table
|
||||
-- set_settings(table) - Set visual settings
|
||||
-- save_settings(path) - Save settings to JSON
|
||||
-- load_settings(path) - Load settings from JSON
|
||||
--
|
||||
-- From Lua, you manipulate the component's fields directly using the
|
||||
-- ecs.set_component / ecs.get_component API.
|
||||
-- Settings table fields:
|
||||
-- font_name, font_size, speaker_font_size,
|
||||
-- background_opacity, box_height_fraction, box_position_fraction
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Create an entity with DialogueComponent
|
||||
-- 1. Show dialogue directly via the singleton API
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local dlg = ecs.create_entity()
|
||||
ecs.set_entity_name(dlg, "DialogueBox")
|
||||
ecs.add_component(dlg, "Dialogue")
|
||||
ecs.dialogue.show("This dialogue was shown directly via the singleton API!",
|
||||
{},
|
||||
"Lua Script")
|
||||
|
||||
print("Dialogue shown via direct API. Active: " .. tostring(ecs.dialogue.is_active()))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Set dialogue text directly via component fields
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Instead of sending an event, you can set the component fields directly.
|
||||
-- The DialogueSystem will pick up the state change on the next frame.
|
||||
|
||||
ecs.set_component(dlg, "Dialogue", {
|
||||
text = "This dialogue was set directly via the component API!",
|
||||
speaker = "Lua Script",
|
||||
enabled = true
|
||||
})
|
||||
|
||||
-- Note: Setting the fields directly does NOT automatically change the state
|
||||
-- to Showing. You need to also set the state, or use the event system.
|
||||
-- The DialogueComponent's show() method handles state transitions.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Read dialogue state from the component
|
||||
-- 2. Read and modify visual settings
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local comp = ecs.get_component(dlg, "Dialogue")
|
||||
if comp then
|
||||
print("Dialogue text: " .. (comp.text or "(empty)"))
|
||||
print("Dialogue speaker: " .. (comp.speaker or "(none)"))
|
||||
print("Dialogue enabled: " .. tostring(comp.enabled))
|
||||
print("Font: " .. (comp.fontName or "default"))
|
||||
print("Font size: " .. (comp.fontSize or 24))
|
||||
end
|
||||
local settings = ecs.dialogue.get_settings()
|
||||
print("Current font: " .. settings.font_name)
|
||||
print("Current font size: " .. settings.font_size)
|
||||
|
||||
-- Change appearance settings
|
||||
settings.font_name = "Jupiteroid-Regular.ttf"
|
||||
settings.font_size = 24.0
|
||||
settings.speaker_font_size = 20.0
|
||||
settings.background_opacity = 0.85
|
||||
settings.box_height_fraction = 0.25
|
||||
settings.box_position_fraction = 0.75
|
||||
|
||||
ecs.dialogue.set_settings(settings)
|
||||
print("Dialogue settings updated")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Modify individual dialogue fields
|
||||
-- 3. Show dialogue with choices
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Change just the text:
|
||||
ecs.set_field(dlg, "Dialogue", "text", "Updated dialogue text!")
|
||||
|
||||
-- Change just the speaker:
|
||||
ecs.set_field(dlg, "Dialogue", "speaker", "Mysterious Stranger")
|
||||
|
||||
-- Change appearance settings:
|
||||
ecs.set_field(dlg, "Dialogue", "backgroundOpacity", 0.9)
|
||||
ecs.set_field(dlg, "Dialogue", "boxHeightFraction", 0.3)
|
||||
ecs.set_field(dlg, "Dialogue", "boxPositionFraction", 0.7)
|
||||
|
||||
-- Read back the changes:
|
||||
local updated_text = ecs.get_field(dlg, "Dialogue", "text")
|
||||
local updated_speaker = ecs.get_field(dlg, "Dialogue", "speaker")
|
||||
print("Updated text: " .. updated_text)
|
||||
print("Updated speaker: " .. updated_speaker)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Toggle dialogue visibility
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Disable the dialogue box:
|
||||
ecs.set_field(dlg, "Dialogue", "enabled", false)
|
||||
print("Dialogue disabled")
|
||||
|
||||
-- Re-enable it:
|
||||
ecs.set_field(dlg, "Dialogue", "enabled", true)
|
||||
print("Dialogue re-enabled")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. Check if dialogue component exists
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
if ecs.has_component(dlg, "Dialogue") then
|
||||
print("Entity has a Dialogue component")
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 7. Remove the dialogue component entirely
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- ecs.remove_component(dlg, "Dialogue")
|
||||
-- print("Dialogue component removed")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 8. Practical: Configure dialogue appearance per-NPC
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function create_npc_with_dialogue(name, mesh, greeting_text)
|
||||
local npc = ecs.create_entity()
|
||||
ecs.set_entity_name(npc, name)
|
||||
|
||||
-- Basic NPC setup
|
||||
ecs.set_component(npc, "Transform", {
|
||||
position = { 0, 0, 0 },
|
||||
rotation = { 1, 0, 0, 0 },
|
||||
scale = { 1, 1, 1 }
|
||||
})
|
||||
|
||||
ecs.set_component(npc, "Renderable", {
|
||||
meshName = mesh or "character.mesh",
|
||||
visible = true
|
||||
})
|
||||
|
||||
-- Dialogue component with NPC-specific appearance
|
||||
ecs.set_component(npc, "Dialogue", {
|
||||
text = greeting_text or "Hello!",
|
||||
speaker = name,
|
||||
fontName = "Jupiteroid-Regular.ttf",
|
||||
fontSize = 24.0,
|
||||
speakerFontSize = 20.0,
|
||||
backgroundOpacity = 0.85,
|
||||
boxHeightFraction = 0.25,
|
||||
boxPositionFraction = 0.75,
|
||||
enabled = true
|
||||
})
|
||||
|
||||
print("Created NPC with dialogue: " .. name)
|
||||
return npc
|
||||
end
|
||||
|
||||
-- Create a few NPCs with different dialogue configurations
|
||||
local merchant = create_npc_with_dialogue(
|
||||
"Merchant",
|
||||
"merchant.mesh",
|
||||
"Welcome to my shop! Best wares in town."
|
||||
ecs.dialogue.show(
|
||||
"Where would you like to travel?",
|
||||
{ "The Forest", "The Village", "The Mountains" },
|
||||
"Guide"
|
||||
)
|
||||
|
||||
local guard = create_npc_with_dialogue(
|
||||
print("Dialogue with choices shown. Active: " .. tostring(ecs.dialogue.is_active()))
|
||||
|
||||
-- Simulate player selecting choice 1
|
||||
ecs.dialogue.select_choice(1)
|
||||
print("After choice: Active = " .. tostring(ecs.dialogue.is_active()))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Show and dismiss (no choices)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ecs.dialogue.show("A mysterious voice echoes through the chamber...")
|
||||
print("Narration shown. Active: " .. tostring(ecs.dialogue.is_active()))
|
||||
|
||||
-- Simulate click-to-progress
|
||||
ecs.dialogue.progress()
|
||||
print("After progress: Active = " .. tostring(ecs.dialogue.is_active()))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Hide dialogue immediately
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
ecs.dialogue.show("This will be cut short.", {}, "Interrupter")
|
||||
ecs.dialogue.hide()
|
||||
print("After hide: Active = " .. tostring(ecs.dialogue.is_active()))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. Save and load settings
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Save current settings to dialogue.json
|
||||
local saved = ecs.dialogue.save_settings("dialogue.json")
|
||||
print("Settings saved: " .. tostring(saved))
|
||||
|
||||
-- Load settings back (or from a different file)
|
||||
local loaded = ecs.dialogue.load_settings("dialogue.json")
|
||||
print("Settings loaded: " .. tostring(loaded))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 7. Practical: Configure dialogue appearance for a scene
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function setup_dialogue_for_scene(scene_type)
|
||||
local scene_settings = ecs.dialogue.get_settings()
|
||||
|
||||
if scene_type == "dark_cave" then
|
||||
scene_settings.background_opacity = 0.95
|
||||
scene_settings.box_height_fraction = 0.20
|
||||
scene_settings.font_size = 22.0
|
||||
elseif scene_type == "bright_outdoor" then
|
||||
scene_settings.background_opacity = 0.70
|
||||
scene_settings.box_height_fraction = 0.30
|
||||
scene_settings.font_size = 26.0
|
||||
elseif scene_type == "intimate_conversation" then
|
||||
scene_settings.background_opacity = 0.90
|
||||
scene_settings.box_height_fraction = 0.22
|
||||
scene_settings.font_size = 24.0
|
||||
scene_settings.speaker_font_size = 22.0
|
||||
end
|
||||
|
||||
ecs.dialogue.set_settings(scene_settings)
|
||||
print("Dialogue configured for scene: " .. scene_type)
|
||||
end
|
||||
|
||||
setup_dialogue_for_scene("dark_cave")
|
||||
setup_dialogue_for_scene("bright_outdoor")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 8. Practical: Show NPC dialogue with dynamic choices
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function show_npc_dialogue(npc_name, text, choices)
|
||||
ecs.dialogue.show(text, choices or {}, npc_name)
|
||||
end
|
||||
|
||||
-- Merchant interaction
|
||||
show_npc_dialogue(
|
||||
"Merchant",
|
||||
"Welcome to my shop! Best wares in town.",
|
||||
{ "Buy Sword (50 gold)", "Buy Shield (30 gold)", "Leave" }
|
||||
)
|
||||
|
||||
-- Guard interaction
|
||||
show_npc_dialogue(
|
||||
"Guard",
|
||||
"guard.mesh",
|
||||
"Halt! Who goes there?"
|
||||
"Halt! Who goes there?",
|
||||
{ "I'm a traveler", "I'm looking for the inn", "None of your business" }
|
||||
)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 9. Practical: Update dialogue based on game events
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function update_npc_dialogue(npc_entity, new_text, new_speaker)
|
||||
-- Update the dialogue text and speaker
|
||||
ecs.set_field(npc_entity, "Dialogue", "text", new_text)
|
||||
if new_speaker then
|
||||
ecs.set_field(npc_entity, "Dialogue", "speaker", new_speaker)
|
||||
function update_dialogue_after_event(event_type)
|
||||
if event_type == "quest_accepted" then
|
||||
ecs.dialogue.show(
|
||||
"Excellent! Bring me the artifact and you'll be rewarded.",
|
||||
{ "Where do I find it?", "I'm on my way!", "Tell me more" },
|
||||
"Quest Giver"
|
||||
)
|
||||
elseif event_type == "combat_start" then
|
||||
ecs.dialogue.show(
|
||||
"Enemies approach! Prepare for battle!",
|
||||
{},
|
||||
"Companion"
|
||||
)
|
||||
elseif event_type == "level_up" then
|
||||
ecs.dialogue.show(
|
||||
"You feel a surge of power! You have reached a new level.",
|
||||
{ "View skills", "Continue" },
|
||||
"System"
|
||||
)
|
||||
end
|
||||
|
||||
-- Show the updated dialogue via event (this triggers the state change)
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = new_text,
|
||||
speaker = new_speaker or ecs.get_field(npc_entity, "Dialogue", "speaker")
|
||||
})
|
||||
end
|
||||
|
||||
-- Update the merchant's dialogue after a transaction
|
||||
update_npc_dialogue(merchant, "Thank you for your business! Come again.")
|
||||
|
||||
-- Update the guard's dialogue when player has high reputation
|
||||
update_npc_dialogue(guard, "At ease, friend. The town is safe with you around.")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 10. Practical: Dialogue with dynamic choices from component data
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function show_dialogue_with_dynamic_choices(npc_entity, base_text, choice_list)
|
||||
-- choice_list is a table of strings
|
||||
|
||||
-- Update the component
|
||||
ecs.set_field(npc_entity, "Dialogue", "text", base_text)
|
||||
|
||||
-- Show via event (which handles state transitions properly)
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = base_text,
|
||||
speaker = ecs.get_field(npc_entity, "Dialogue", "speaker"),
|
||||
choices = choice_list
|
||||
})
|
||||
end
|
||||
|
||||
-- Example: Shop inventory as dialogue choices
|
||||
local shop_items = { "Buy Sword (50 gold)", "Buy Shield (30 gold)", "Buy Potion (10 gold)", "Leave" }
|
||||
show_dialogue_with_dynamic_choices(merchant, "What would you like to buy?", shop_items)
|
||||
update_dialogue_after_event("quest_accepted")
|
||||
|
||||
-- =============================================================================
|
||||
-- Summary
|
||||
-- =============================================================================
|
||||
-- Direct component API vs EventBus approach:
|
||||
-- Direct singleton API vs EventBus approach:
|
||||
--
|
||||
-- Component API (ecs.set_component / ecs.get_component):
|
||||
-- - Read/write any DialogueComponent field
|
||||
-- - Configure appearance (font, size, opacity, position)
|
||||
-- - Toggle enabled/disabled
|
||||
-- - Does NOT trigger state transitions (Showing/AwaitingChoice/Idle)
|
||||
-- Singleton API (ecs.dialogue.*):
|
||||
-- - Direct control over showing/hiding/progressing/selecting
|
||||
-- - Read/write visual settings (font, size, opacity, position)
|
||||
-- - Save/load settings to dialogue.json
|
||||
-- - Best for scripted sequences and direct game logic
|
||||
--
|
||||
-- EventBus (ecs.send_event "dialogue_show"):
|
||||
-- - Triggers proper state transitions
|
||||
-- - Parses choices from table of strings
|
||||
-- - Best for showing dialogue to the player
|
||||
-- - Triggers dialogue via the event system
|
||||
-- - Good for decoupled systems (e.g., NPCs, triggers, quests)
|
||||
-- - Same underlying singleton, just a different entry point
|
||||
--
|
||||
-- Best practice: Use the EventBus to SHOW dialogue, and the component API
|
||||
-- to CONFIGURE the dialogue box appearance.
|
||||
-- Best practice: Use the singleton API for direct control, and EventBus
|
||||
-- for triggering dialogue from other systems.
|
||||
-- =============================================================================
|
||||
|
||||
print("Dialogue component API examples completed!")
|
||||
print("Dialogue singleton API examples completed!")
|
||||
|
||||
@@ -12,19 +12,12 @@
|
||||
-- sequences where one event triggers dialogue, and the player's choice
|
||||
-- triggers another event.
|
||||
--
|
||||
-- Event parameters use the EventParams type with flat key-value pairs.
|
||||
-- Note: DialogueSystem is a singleton. No ECS DialogueComponent is needed.
|
||||
-- Dialogue is shown via ecs.dialogue.show() or ecs.send_event().
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Create the dialogue entity
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local dlg = ecs.create_entity()
|
||||
ecs.set_entity_name(dlg, "DialogueBox")
|
||||
ecs.add_component(dlg, "Dialogue")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Create an NPC with EventHandler for dialogue triggers
|
||||
-- 1. Create an NPC with EventHandler for dialogue triggers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local npc = ecs.create_entity()
|
||||
@@ -51,7 +44,7 @@ ecs.set_component(npc, "EventHandler", {
|
||||
print("Created NPC with EventHandler for player_approached event")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Create an EventHandler that triggers on quest completion
|
||||
-- 2. Create an EventHandler that triggers on quest completion
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local quest_npc = ecs.create_entity()
|
||||
@@ -66,7 +59,7 @@ ecs.set_component(quest_npc, "EventHandler", {
|
||||
print("Created NPC with EventHandler for quest_completed event")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Trigger dialogue via events from other game systems
|
||||
-- 3. Trigger dialogue via events from other game systems
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Simulate a proximity trigger: when the player gets close to an NPC,
|
||||
@@ -83,12 +76,12 @@ function on_player_near_npc(npc_name, distance)
|
||||
distance = distance
|
||||
})
|
||||
|
||||
-- Also show dialogue directly
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = "Hello there! I have a quest for a brave adventurer.",
|
||||
speaker = npc_name,
|
||||
choices = { "I'll help!", "What's the reward?", "Not interested" }
|
||||
})
|
||||
-- Also show dialogue directly via singleton API
|
||||
ecs.dialogue.show(
|
||||
"Hello there! I have a quest for a brave adventurer.",
|
||||
{ "I'll help!", "What's the reward?", "Not interested" },
|
||||
npc_name
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -96,15 +89,17 @@ end
|
||||
on_player_near_npc("QuestGiver", 3.0)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Chain events: choice -> event -> next dialogue
|
||||
-- 4. Chain events: choice -> event -> next dialogue
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- When the player makes a choice, we can send a new event that triggers
|
||||
-- another EventHandler, creating a chain reaction.
|
||||
|
||||
-- Subscribe to dialogue choices
|
||||
local choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params)
|
||||
local choice_index = params.choice_index or 0
|
||||
local choice_text = params.choice_text or ""
|
||||
-- Since choice selection happens via C++ callback, in a real game you'd
|
||||
-- wire the callback to send events. From Lua, we can demonstrate by
|
||||
-- sending follow-up events manually.
|
||||
|
||||
function on_dialogue_choice_made(choice_text)
|
||||
print("Player chose: " .. choice_text)
|
||||
|
||||
if choice_text == "I'll help!" then
|
||||
-- Player accepted the quest - trigger quest acceptance event
|
||||
@@ -116,31 +111,34 @@ local choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params
|
||||
})
|
||||
|
||||
-- Show follow-up dialogue
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = "Excellent! The ancient artifact was stolen from the temple.\nBring it back and you'll be richly rewarded!",
|
||||
speaker = "QuestGiver",
|
||||
choices = { "Where is the temple?", "I'm on it!", "Tell me more" }
|
||||
})
|
||||
ecs.dialogue.show(
|
||||
"Excellent! The ancient artifact was stolen from the temple.\nBring it back and you'll be richly rewarded!",
|
||||
{ "Where is the temple?", "I'm on it!", "Tell me more" },
|
||||
"QuestGiver"
|
||||
)
|
||||
|
||||
elseif choice_text == "What's the reward?" then
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = "100 gold pieces and a magical amulet! What do you say?",
|
||||
speaker = "QuestGiver",
|
||||
choices = { "I'll help!", "Sounds good", "Maybe later" }
|
||||
})
|
||||
ecs.dialogue.show(
|
||||
"100 gold pieces and a magical amulet! What do you say?",
|
||||
{ "I'll help!", "Sounds good", "Maybe later" },
|
||||
"QuestGiver"
|
||||
)
|
||||
|
||||
elseif choice_text == "Not interested" then
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = "Very well. The offer stands if you change your mind.",
|
||||
speaker = "QuestGiver"
|
||||
})
|
||||
ecs.dialogue.show(
|
||||
"Very well. The offer stands if you change your mind.",
|
||||
{},
|
||||
"QuestGiver"
|
||||
)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
print("Subscribed to dialogue_choice for event chaining")
|
||||
-- Simulate choices
|
||||
on_dialogue_choice_made("I'll help!")
|
||||
on_dialogue_choice_made("What's the reward?")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. Subscribe to custom events for game logic
|
||||
-- 5. Subscribe to custom events for game logic
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Listen for quest acceptance
|
||||
@@ -162,7 +160,7 @@ end)
|
||||
print("Subscribed to quest_accepted events")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 7. Practical: Zone entry dialogue
|
||||
-- 6. Practical: Zone entry dialogue
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- When the player enters a new area, show contextual dialogue.
|
||||
|
||||
@@ -188,10 +186,7 @@ function on_zone_entered(zone_name)
|
||||
|
||||
local dialogue = zone_dialogues[zone_name]
|
||||
if dialogue then
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = dialogue.text,
|
||||
speaker = dialogue.speaker
|
||||
})
|
||||
ecs.dialogue.show(dialogue.text, {}, dialogue.speaker)
|
||||
|
||||
-- Also send a zone-specific event for other systems
|
||||
ecs.send_event("zone_entered", {
|
||||
@@ -206,7 +201,7 @@ on_zone_entered("village")
|
||||
on_zone_entered("dungeon")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 8. Practical: Item pickup dialogue
|
||||
-- 7. Practical: Item pickup dialogue
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function on_item_picked_up(item_name, item_count)
|
||||
@@ -220,10 +215,7 @@ function on_item_picked_up(item_name, item_count)
|
||||
|
||||
local message = pickup_messages[item_name]
|
||||
if message then
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = message,
|
||||
speaker = "Narrator"
|
||||
})
|
||||
ecs.dialogue.show(message, {}, "Narrator")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -253,8 +245,8 @@ on_item_picked_up("gold_coins", 50)
|
||||
-- Accept quest -> event -> update quest log -> next dialogue
|
||||
-- Complete quest -> event -> reward dialogue -> next dialogue
|
||||
--
|
||||
-- EventParams uses flat key-value pairs. Type metadata is available
|
||||
-- via params._types table (e.g., params._types.reward_gold = "int").
|
||||
-- No ECS DialogueComponent needed — use ecs.dialogue.show() or
|
||||
-- ecs.send_event("dialogue_show", { ... }) to display dialogue.
|
||||
-- =============================================================================
|
||||
|
||||
print("Dialogue EventHandler integration examples completed!")
|
||||
|
||||
@@ -9,135 +9,112 @@
|
||||
--
|
||||
-- Dialogue-related events you can subscribe to:
|
||||
-- "dialogue_show" - Fired when dialogue should be displayed
|
||||
-- "dialogue_choice" - Fired when player selects a choice
|
||||
-- "dialogue_dismiss" - Fired when dialogue is dismissed (no choices)
|
||||
-- "dialogue_hide" - Fired when dialogue is hidden
|
||||
--
|
||||
-- Event parameters use the EventParams type, which supports flat
|
||||
-- key-value pairs with typed values. Use params._types to check types.
|
||||
-- Note: choice and dismiss callbacks are handled via the singleton's
|
||||
-- onChoiceSelected / onDismissed callbacks (C++ side). From Lua, you
|
||||
-- can poll is_active() or subscribe to your own custom events.
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Create the dialogue entity
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local dialogue_entity = ecs.create_entity()
|
||||
ecs.set_entity_name(dialogue_entity, "DialogueBox")
|
||||
ecs.add_component(dialogue_entity, "Dialogue")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Subscribe to dialogue choice events
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- When the player selects a choice in the dialogue box, we can react to it.
|
||||
-- The DialogueComponent's onChoiceSelected callback fires with the 1-based
|
||||
-- choice index. We bridge this via the EventBus.
|
||||
|
||||
local choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params)
|
||||
local choice_index = params.choice_index or 0
|
||||
local choice_text = params.choice_text or "unknown"
|
||||
|
||||
print("Player selected choice #" .. choice_index .. ": " .. choice_text)
|
||||
|
||||
-- React based on which choice was selected
|
||||
if choice_index == 1 then
|
||||
print(" -> Player chose the first option!")
|
||||
elseif choice_index == 2 then
|
||||
print(" -> Player chose the second option!")
|
||||
elseif choice_index == 3 then
|
||||
print(" -> Player chose the third option!")
|
||||
end
|
||||
end)
|
||||
|
||||
print("Subscribed to dialogue_choice events (ID: " .. choice_sub .. ")")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Subscribe to dialogue dismiss events
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- When dialogue is dismissed (clicked through with no choices), we can
|
||||
-- trigger follow-up actions.
|
||||
|
||||
local dismiss_sub = ecs.subscribe_event("dialogue_dismiss", function(event, params)
|
||||
print("Dialogue was dismissed by the player")
|
||||
|
||||
-- You could trigger follow-up dialogue or game logic here
|
||||
local next_text = params.next_text or ""
|
||||
if next_text ~= "" then
|
||||
print(" -> Next dialogue queued: " .. next_text)
|
||||
end
|
||||
end)
|
||||
|
||||
print("Subscribed to dialogue_dismiss events (ID: " .. dismiss_sub .. ")")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Subscribe to dialogue show events (for logging/tracking)
|
||||
-- 1. Subscribe to dialogue show events (for logging/tracking)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local show_sub = ecs.subscribe_event("dialogue_show", function(event, params)
|
||||
local text = params.text or ""
|
||||
local speaker = params.speaker or "Unknown"
|
||||
local choices = params.choices or {}
|
||||
|
||||
print("[Dialogue Log] " .. speaker .. ": \"" .. text .. "\"")
|
||||
|
||||
if #choices > 0 then
|
||||
print("[Dialogue Log] Choices: " .. table.concat(choices, ", "))
|
||||
end
|
||||
end)
|
||||
|
||||
print("Subscribed to dialogue_show events for logging (ID: " .. show_sub .. ")")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Example: Branching dialogue with choice handling
|
||||
-- 2. Subscribe to dialogue hide events
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- This shows a complete flow: show dialogue -> handle choice -> react
|
||||
|
||||
local hide_sub = ecs.subscribe_event("dialogue_hide", function(event, params)
|
||||
print("[Dialogue Log] Dialogue was hidden")
|
||||
end)
|
||||
|
||||
print("Subscribed to dialogue_hide events (ID: " .. hide_sub .. ")")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Example: Branching dialogue with choice handling
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- This shows a complete flow: show dialogue -> handle choice -> react.
|
||||
-- Since choice selection happens via C++ callbacks, from Lua we can
|
||||
-- use a custom event pattern or poll is_active().
|
||||
|
||||
function show_branching_dialogue()
|
||||
-- Step 1: Show the dialogue with choices
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = "You see a dark cave entrance. What do you do?",
|
||||
speaker = "Narrator",
|
||||
choices = { "Enter the cave", "Look around first", "Leave" }
|
||||
})
|
||||
ecs.dialogue.show(
|
||||
"You see a dark cave entrance. What do you do?",
|
||||
{ "Enter the cave", "Look around first", "Leave" },
|
||||
"Narrator"
|
||||
)
|
||||
|
||||
-- Step 2: The choice will be handled by our subscriber above.
|
||||
-- In a real scenario, you'd use a state machine or coroutine to
|
||||
-- manage the flow. See dialogue_sequence.lua for a more advanced example.
|
||||
print("Branching dialogue shown. Waiting for player choice...")
|
||||
|
||||
-- Step 2: In a real game, you'd check the result asynchronously.
|
||||
-- For this example, we demonstrate the pattern.
|
||||
-- (Use ecs.dialogue.is_active() to poll, or wire C++ callbacks
|
||||
-- to send custom events when choices are made.)
|
||||
end
|
||||
|
||||
show_branching_dialogue()
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. Example: NPC greeting with follow-up
|
||||
-- 4. Example: NPC greeting with follow-up
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function npc_greeting(npc_name, greeting_text)
|
||||
-- Show initial greeting
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = greeting_text,
|
||||
speaker = npc_name,
|
||||
choices = { "Who are you?", "Tell me about this place", "Goodbye" }
|
||||
})
|
||||
ecs.dialogue.show(
|
||||
greeting_text,
|
||||
{ "Who are you?", "Tell me about this place", "Goodbye" },
|
||||
npc_name
|
||||
)
|
||||
|
||||
-- The choice subscriber will handle the response.
|
||||
-- You could extend this with a lookup table for NPC responses.
|
||||
-- The choice handling would be done via C++ callback wiring or
|
||||
-- by polling is_active() in your game loop.
|
||||
end
|
||||
|
||||
npc_greeting("Elder Marcus", "Ah, a new face in our village! Welcome, traveler.")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Example: Chain multiple dialogue lines
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function show_dialogue_sequence(lines)
|
||||
for i, line in ipairs(lines) do
|
||||
ecs.dialogue.show(line.text, line.choices or {}, line.speaker or "")
|
||||
print(" [Line " .. i .. "] " .. (line.speaker or "") .. ": \"" .. line.text .. "\"")
|
||||
end
|
||||
end
|
||||
|
||||
local intro_sequence = {
|
||||
{ text = "The storm rages outside.", speaker = "Narrator" },
|
||||
{ text = "You find shelter in an abandoned tower.", speaker = "Narrator" },
|
||||
{ text = "A voice calls from the shadows...", speaker = "Narrator" },
|
||||
{ text = "Who dares enter my sanctuary?", speaker = "Mysterious Voice",
|
||||
choices = { "I seek shelter from the storm", "I mean no harm", "I was sent here" } }
|
||||
}
|
||||
|
||||
show_dialogue_sequence(intro_sequence)
|
||||
|
||||
-- =============================================================================
|
||||
-- Summary
|
||||
-- =============================================================================
|
||||
-- To handle dialogue choices from Lua:
|
||||
-- 1. Subscribe to "dialogue_choice" events
|
||||
-- 2. Check params.choice_index (1-based) to see which was picked
|
||||
-- 3. Check params.choice_text for the label text
|
||||
-- 4. React accordingly in your game logic
|
||||
--
|
||||
-- To handle dialogue dismissal:
|
||||
-- 1. Subscribe to "dialogue_dismiss" events
|
||||
-- 2. Trigger follow-up actions as needed
|
||||
-- To handle dialogue events from Lua:
|
||||
-- 1. Subscribe to "dialogue_show" events for logging or side effects
|
||||
-- 2. Subscribe to "dialogue_hide" events for cleanup
|
||||
-- 3. Use ecs.dialogue.show() / ecs.dialogue.hide() for direct control
|
||||
-- 4. For choice reactions, wire C++ callbacks to Lua events, or
|
||||
-- poll ecs.dialogue.is_active() in your update loop
|
||||
--
|
||||
-- EventParams uses flat key-value pairs. Type metadata is available
|
||||
-- via params._types table (e.g., params._types.choice_index = "int").
|
||||
-- via params._types table (e.g., params._types.text = "string").
|
||||
-- =============================================================================
|
||||
|
||||
print("Dialogue event subscription examples completed!")
|
||||
|
||||
@@ -7,21 +7,15 @@
|
||||
--
|
||||
-- The pattern:
|
||||
-- 1. Show dialogue with choices
|
||||
-- 2. Wait for player to select a choice (via event subscription)
|
||||
-- 2. Wait for player to select a choice (via callback or polling)
|
||||
-- 3. React and show next dialogue based on the choice
|
||||
-- 4. Repeat until the conversation ends
|
||||
--
|
||||
-- Note: DialogueSystem is now a singleton. No ECS entity/component needed.
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Create the dialogue entity
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local dialogue_entity = ecs.create_entity()
|
||||
ecs.set_entity_name(dialogue_entity, "DialogueBox")
|
||||
ecs.add_component(dialogue_entity, "Dialogue")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Dialogue Queue System
|
||||
-- 1. Dialogue Queue System
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- A simple queue that lets you chain dialogue lines and wait for player
|
||||
-- input between each one.
|
||||
@@ -32,25 +26,12 @@ local dialogue_queue_pending = false
|
||||
local dialogue_queue_choice = 0
|
||||
local dialogue_queue_choice_text = ""
|
||||
|
||||
-- Subscribe to choice events to unblock the queue
|
||||
local queue_choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params)
|
||||
if dialogue_queue_pending then
|
||||
dialogue_queue_choice = params.choice_index or 0
|
||||
dialogue_queue_choice_text = params.choice_text or ""
|
||||
dialogue_queue_pending = false
|
||||
end
|
||||
end)
|
||||
|
||||
-- Subscribe to dismiss events to unblock the queue
|
||||
local queue_dismiss_sub = ecs.subscribe_event("dialogue_dismiss", function(event, params)
|
||||
if dialogue_queue_pending then
|
||||
dialogue_queue_choice = -1 -- signal dismissed
|
||||
dialogue_queue_pending = false
|
||||
end
|
||||
end)
|
||||
-- Since choice selection is handled via C++ callbacks, in a real game
|
||||
-- you'd wire those callbacks to set these variables. For this example,
|
||||
-- we demonstrate the pattern using polling.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Helper: Show dialogue and wait for player response
|
||||
-- 2. Helper: Show dialogue and wait for player response
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Show a line of dialogue and wait for the player to respond.
|
||||
@@ -59,12 +40,8 @@ end)
|
||||
--- @param choices table|nil Array of choice label strings (nil = click to dismiss)
|
||||
--- @return number choice_index (0 if dismissed, 1+ for choices)
|
||||
function show_and_wait(text, speaker, choices)
|
||||
-- Send the dialogue event
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = text,
|
||||
speaker = speaker or "",
|
||||
choices = choices or {}
|
||||
})
|
||||
-- Show dialogue via the singleton API
|
||||
ecs.dialogue.show(text, choices or {}, speaker or "")
|
||||
|
||||
-- Wait for player response
|
||||
dialogue_queue_pending = true
|
||||
@@ -76,6 +53,9 @@ function show_and_wait(text, speaker, choices)
|
||||
while dialogue_queue_pending and timeout > 0 do
|
||||
-- In a real game loop, this would be a coroutine yield.
|
||||
-- For this example, we simulate with a counter.
|
||||
if not ecs.dialogue.is_active() then
|
||||
dialogue_queue_pending = false
|
||||
end
|
||||
timeout = timeout - 1
|
||||
if timeout <= 0 then
|
||||
dialogue_queue_pending = false
|
||||
@@ -87,33 +67,34 @@ function show_and_wait(text, speaker, choices)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Example: Simple linear conversation
|
||||
-- 3. Example: Simple linear conversation
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function simple_conversation()
|
||||
print("=== Simple Conversation ===")
|
||||
|
||||
-- Line 1: Narration with no choices (click to continue)
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = "The old man sits by the fire, staring into the flames.",
|
||||
speaker = "Narrator"
|
||||
})
|
||||
ecs.dialogue.show(
|
||||
"The old man sits by the fire, staring into the flames.",
|
||||
{},
|
||||
"Narrator"
|
||||
)
|
||||
|
||||
-- In a real game, you'd wait for the dismiss event here.
|
||||
-- For this example, we just show the pattern.
|
||||
|
||||
-- Line 2: NPC speaks with choices
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = "I've been expecting you. The darkness grows stronger each day.",
|
||||
speaker = "Old Man",
|
||||
choices = { "Tell me more", "How can I help?", "I must go" }
|
||||
})
|
||||
ecs.dialogue.show(
|
||||
"I've been expecting you. The darkness grows stronger each day.",
|
||||
{ "Tell me more", "How can I help?", "I must go" },
|
||||
"Old Man"
|
||||
)
|
||||
|
||||
print(" (Player would now see choices and pick one)")
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Example: Branching conversation tree
|
||||
-- 4. Example: Branching conversation tree
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Define a conversation tree as a table of nodes
|
||||
@@ -222,12 +203,8 @@ function run_conversation(tree, start_node)
|
||||
end
|
||||
end
|
||||
|
||||
-- Show the dialogue
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = node.text,
|
||||
speaker = node.speaker or "",
|
||||
choices = choices
|
||||
})
|
||||
-- Show the dialogue via singleton API
|
||||
ecs.dialogue.show(node.text, choices, node.speaker or "")
|
||||
|
||||
-- In a real game, you'd wait for the player's choice here.
|
||||
-- For this example, we simulate by picking the first choice.
|
||||
@@ -250,7 +227,7 @@ end
|
||||
run_conversation(conversations.village_elder, "greeting")
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. Example: NPC dialogue with state tracking
|
||||
-- 5. Example: NPC dialogue with state tracking
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Track NPC dialogue state
|
||||
@@ -267,34 +244,34 @@ function talk_to_elder_marcus()
|
||||
npc_state.marcus_met = true
|
||||
npc_state.marcus_friendship = 10
|
||||
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = "Ah, a new face! I am Elder Marcus, keeper of this village.\nIt's been so long since we had visitors.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = { "Pleasure to meet you", "I've heard stories about you", "Hello" }
|
||||
})
|
||||
ecs.dialogue.show(
|
||||
"Ah, a new face! I am Elder Marcus, keeper of this village.\nIt's been so long since we had visitors.",
|
||||
{ "Pleasure to meet you", "I've heard stories about you", "Hello" },
|
||||
"Elder Marcus"
|
||||
)
|
||||
elseif npc_state.quest_active and npc_state.quest_completed then
|
||||
-- Quest completed
|
||||
npc_state.marcus_friendship = npc_state.marcus_friendship + 50
|
||||
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = "You did it! The village is safe thanks to you.\nPlease, take this reward.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = { "Thank you, elder", "I was happy to help" }
|
||||
})
|
||||
ecs.dialogue.show(
|
||||
"You did it! The village is safe thanks to you.\nPlease, take this reward.",
|
||||
{ "Thank you, elder", "I was happy to help" },
|
||||
"Elder Marcus"
|
||||
)
|
||||
elseif npc_state.quest_active then
|
||||
-- Quest in progress
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = "Have you dealt with those bandits yet?\nThe villagers are growing anxious.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = { "I'm working on it", "I need more information", "Not yet" }
|
||||
})
|
||||
ecs.dialogue.show(
|
||||
"Have you dealt with those bandits yet?\nThe villagers are growing anxious.",
|
||||
{ "I'm working on it", "I need more information", "Not yet" },
|
||||
"Elder Marcus"
|
||||
)
|
||||
else
|
||||
-- Regular greeting
|
||||
ecs.send_event("dialogue_show", {
|
||||
text = "Welcome back, friend. The village is peaceful today.",
|
||||
speaker = "Elder Marcus",
|
||||
choices = { "Any news?", "I need supplies", "Goodbye" }
|
||||
})
|
||||
ecs.dialogue.show(
|
||||
"Welcome back, friend. The village is peaceful today.",
|
||||
{ "Any news?", "I need supplies", "Goodbye" },
|
||||
"Elder Marcus"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -310,11 +287,11 @@ talk_to_elder_marcus() -- Quest completed
|
||||
-- Summary
|
||||
-- =============================================================================
|
||||
-- For sequential dialogue:
|
||||
-- 1. Use a queue/coroutine pattern to chain dialogue lines
|
||||
-- 2. Subscribe to "dialogue_choice" and "dialogue_dismiss" events
|
||||
-- 3. Wait for player input between each line
|
||||
-- 4. Use conversation trees for branching narratives
|
||||
-- 5. Track NPC state to change dialogue based on game progress
|
||||
-- 1. Use ecs.dialogue.show() to display each line
|
||||
-- 2. Wait for player input between lines (callback or polling)
|
||||
-- 3. Use conversation trees for branching narratives
|
||||
-- 4. Track NPC state to change dialogue based on game progress
|
||||
-- 5. No ECS entity needed — DialogueSystem is a singleton
|
||||
-- =============================================================================
|
||||
|
||||
print("Dialogue sequence examples completed!")
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
#include "LuaCharacterClassApi.hpp"
|
||||
#include "../systems/CharacterClassSystem.hpp"
|
||||
#include "../components/CharacterClassDatabase.hpp"
|
||||
#include "../components/CharacterClassComponent.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: get the Flecs world from the Lua registry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static flecs::world getWorld(lua_State *L)
|
||||
{
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld");
|
||||
OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered");
|
||||
flecs::world *world =
|
||||
static_cast<flecs::world *>(lua_touserdata(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return *world;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push string-vector as Lua table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void pushStringVector(lua_State *L,
|
||||
const std::vector<Ogre::String> &vec)
|
||||
{
|
||||
lua_newtable(L);
|
||||
for (size_t i = 0; i < vec.size(); i++) {
|
||||
lua_pushstring(L, vec[i].c_str());
|
||||
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Database queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaGetClassNames(lua_State *L)
|
||||
{
|
||||
pushStringVector(L,
|
||||
CharacterClassDatabase::getSingleton()
|
||||
.getClassNames());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGetStatNames(lua_State *L)
|
||||
{
|
||||
pushStringVector(L,
|
||||
CharacterClassDatabase::getSingleton()
|
||||
.getStatNames());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGetSkillNames(lua_State *L)
|
||||
{
|
||||
pushStringVector(L,
|
||||
CharacterClassDatabase::getSingleton()
|
||||
.getSkillNames());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGetNeedNames(lua_State *L)
|
||||
{
|
||||
pushStringVector(L,
|
||||
CharacterClassDatabase::getSingleton()
|
||||
.getNeedNames());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGetClass(lua_State *L)
|
||||
{
|
||||
const char *name = lua_tostring(L, 1);
|
||||
if (!name)
|
||||
return 0;
|
||||
|
||||
const auto *cls = CharacterClassDatabase::getSingleton().findClass(
|
||||
name);
|
||||
if (!cls)
|
||||
return 0;
|
||||
|
||||
lua_newtable(L);
|
||||
lua_pushstring(L, cls->name.c_str());
|
||||
lua_setfield(L, -2, "name");
|
||||
lua_pushstring(L, cls->description.c_str());
|
||||
lua_setfield(L, -2, "description");
|
||||
pushStringVector(L, cls->primaryStats);
|
||||
lua_setfield(L, -2, "primary_stats");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGetStatKind(lua_State *L)
|
||||
{
|
||||
const char *name = lua_tostring(L, 1);
|
||||
if (!name) {
|
||||
lua_pushstring(L, "unknown");
|
||||
return 1;
|
||||
}
|
||||
const auto *def = CharacterClassDatabase::getSingleton().findStat(name);
|
||||
if (!def) {
|
||||
lua_pushstring(L, "unknown");
|
||||
return 1;
|
||||
}
|
||||
lua_pushstring(L, def->kind == CharacterClassDatabase::StatKind::ResourcePool ?
|
||||
"resource_pool" : "attribute");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Per-entity runtime API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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);
|
||||
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);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaAddXP(lua_State *L)
|
||||
{
|
||||
int entityId = static_cast<int>(lua_tointeger(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>()) {
|
||||
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;
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGetStat(lua_State *L)
|
||||
{
|
||||
int entityId = static_cast<int>(lua_tointeger(L, 1));
|
||||
const char *statName = lua_tostring(L, 2);
|
||||
flecs::entity e = getWorld(L).entity(entityId);
|
||||
if (!e.is_alive() || !e.has<CharacterClassComponent>() ||
|
||||
!statName) {
|
||||
lua_pushinteger(L, 0);
|
||||
return 1;
|
||||
}
|
||||
lua_pushinteger(L,
|
||||
(lua_Integer)e.get<CharacterClassComponent>().getStat(
|
||||
statName));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGetSkill(lua_State *L)
|
||||
{
|
||||
int entityId = static_cast<int>(lua_tointeger(L, 1));
|
||||
const char *skillName = lua_tostring(L, 2);
|
||||
flecs::entity e = getWorld(L).entity(entityId);
|
||||
if (!e.is_alive() || !e.has<CharacterClassComponent>() ||
|
||||
!skillName) {
|
||||
lua_pushinteger(L, 0);
|
||||
return 1;
|
||||
}
|
||||
lua_pushinteger(L,
|
||||
(lua_Integer)e.get<CharacterClassComponent>().getSkill(
|
||||
skillName));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGetNeed(lua_State *L)
|
||||
{
|
||||
int entityId = static_cast<int>(lua_tointeger(L, 1));
|
||||
const char *needName = lua_tostring(L, 2);
|
||||
flecs::entity e = getWorld(L).entity(entityId);
|
||||
if (!e.is_alive() || !e.has<CharacterClassComponent>() ||
|
||||
!needName) {
|
||||
lua_pushinteger(L, 0);
|
||||
return 1;
|
||||
}
|
||||
lua_pushinteger(L,
|
||||
(lua_Integer)e.get<CharacterClassComponent>().getNeed(
|
||||
needName));
|
||||
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);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaSetNeed(lua_State *L)
|
||||
{
|
||||
int entityId = static_cast<int>(lua_tointeger(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) {
|
||||
return 0;
|
||||
}
|
||||
auto &cc = e.get_mut<CharacterClassComponent>();
|
||||
cc.needs[needName] = value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int luaGetPoolCurrent(lua_State *L)
|
||||
{
|
||||
int entityId = static_cast<int>(lua_tointeger(L, 1));
|
||||
const char *poolName = lua_tostring(L, 2);
|
||||
flecs::entity e = getWorld(L).entity(entityId);
|
||||
if (!e.is_alive() || !e.has<CharacterClassComponent>() ||
|
||||
!poolName) {
|
||||
lua_pushinteger(L, 0);
|
||||
return 1;
|
||||
}
|
||||
lua_pushinteger(L, (lua_Integer)e.get<CharacterClassComponent>().
|
||||
getPoolCurrent(poolName));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGetPoolMax(lua_State *L)
|
||||
{
|
||||
int entityId = static_cast<int>(lua_tointeger(L, 1));
|
||||
const char *poolName = lua_tostring(L, 2);
|
||||
flecs::entity e = getWorld(L).entity(entityId);
|
||||
if (!e.is_alive() || !e.has<CharacterClassComponent>() ||
|
||||
!poolName) {
|
||||
lua_pushinteger(L, 0);
|
||||
return 1;
|
||||
}
|
||||
lua_pushinteger(L, (lua_Integer)e.get<CharacterClassComponent>().
|
||||
getPoolMax(poolName));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaSetPoolCurrent(lua_State *L)
|
||||
{
|
||||
int entityId = static_cast<int>(lua_tointeger(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) {
|
||||
lua_pushboolean(L, 0);
|
||||
return 1;
|
||||
}
|
||||
e.get_mut<CharacterClassComponent>().setPoolCurrent(poolName, value);
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaCharacterClassApi(lua_State *L)
|
||||
{
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
}
|
||||
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushcfunction(L, luaGetClassNames);
|
||||
lua_setfield(L, -2, "get_class_names");
|
||||
|
||||
lua_pushcfunction(L, luaGetStatNames);
|
||||
lua_setfield(L, -2, "get_stat_names");
|
||||
|
||||
lua_pushcfunction(L, luaGetSkillNames);
|
||||
lua_setfield(L, -2, "get_skill_names");
|
||||
|
||||
lua_pushcfunction(L, luaGetNeedNames);
|
||||
lua_setfield(L, -2, "get_need_names");
|
||||
|
||||
lua_pushcfunction(L, luaGetClass);
|
||||
lua_setfield(L, -2, "get_class");
|
||||
|
||||
lua_pushcfunction(L, luaGetStatKind);
|
||||
lua_setfield(L, -2, "get_stat_kind");
|
||||
|
||||
lua_pushcfunction(L, luaGetLevel);
|
||||
lua_setfield(L, -2, "get_level");
|
||||
|
||||
lua_pushcfunction(L, luaGetXP);
|
||||
lua_setfield(L, -2, "get_xp");
|
||||
|
||||
lua_pushcfunction(L, luaAddXP);
|
||||
lua_setfield(L, -2, "add_xp");
|
||||
|
||||
lua_pushcfunction(L, luaGetStat);
|
||||
lua_setfield(L, -2, "get_stat");
|
||||
|
||||
lua_pushcfunction(L, luaGetSkill);
|
||||
lua_setfield(L, -2, "get_skill");
|
||||
|
||||
lua_pushcfunction(L, luaGetNeed);
|
||||
lua_setfield(L, -2, "get_need");
|
||||
|
||||
lua_pushcfunction(L, luaGetAvailablePoints);
|
||||
lua_setfield(L, -2, "get_available_points");
|
||||
|
||||
lua_pushcfunction(L, luaSetNeed);
|
||||
lua_setfield(L, -2, "set_need");
|
||||
|
||||
lua_pushcfunction(L, luaGetPoolCurrent);
|
||||
lua_setfield(L, -2, "get_pool_current");
|
||||
|
||||
lua_pushcfunction(L, luaGetPoolMax);
|
||||
lua_setfield(L, -2, "get_pool_max");
|
||||
|
||||
lua_pushcfunction(L, luaSetPoolCurrent);
|
||||
lua_setfield(L, -2, "set_pool_current");
|
||||
|
||||
lua_setfield(L, -2, "character_class");
|
||||
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef EDITSCENE_LUA_CHARACTER_CLASS_API_HPP
|
||||
#define EDITSCENE_LUA_CHARACTER_CLASS_API_HPP
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
void registerLuaCharacterClassApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_CHARACTER_CLASS_API_HPP
|
||||
@@ -37,7 +37,6 @@
|
||||
#include "components/AnimationTreeTemplate.hpp"
|
||||
#include "components/Character.hpp"
|
||||
#include "components/StartupMenu.hpp"
|
||||
#include "components/DialogueComponent.hpp"
|
||||
#include "components/PlayerController.hpp"
|
||||
#include "components/CellGrid.hpp"
|
||||
#include "components/ActionDatabase.hpp"
|
||||
@@ -500,6 +499,21 @@ static void registerAllComponents()
|
||||
lua_pushstring(L, kv.second.c_str());
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
} lua_setfield(L, -2, "slots");
|
||||
// slotSelections map: push as nested table
|
||||
lua_newtable(L);
|
||||
for (auto &kv : c.slotSelections) {
|
||||
lua_newtable(L);
|
||||
lua_pushstring(L, kv.second.layer1Mesh.c_str());
|
||||
lua_setfield(L, -2, "layer1Mesh");
|
||||
lua_pushstring(L, kv.second.layer2Mesh.c_str());
|
||||
lua_setfield(L, -2, "layer2Mesh");
|
||||
lua_pushstring(L, kv.second.explicitMesh.c_str());
|
||||
lua_setfield(L, -2, "explicitMesh");
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "slotSelections");
|
||||
lua_pushinteger(L, c.outfitLevel);
|
||||
lua_setfield(L, -2, "outfitLevel");
|
||||
pushVector3(L, c.frontAxis); lua_setfield(L, -2, "frontAxis");
|
||||
, if (lua_getfield(L, idx, "age"), lua_isstring(L, -1))
|
||||
c.age = lua_tostring(L, -1);
|
||||
@@ -507,6 +521,9 @@ static void registerAllComponents()
|
||||
if (lua_getfield(L, idx, "sex"), lua_isstring(L, -1))
|
||||
c.sex = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "outfitLevel"), lua_isnumber(L, -1))
|
||||
c.outfitLevel = lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "slots"), lua_istable(L, -1)) {
|
||||
c.slots.clear();
|
||||
lua_pushnil(L);
|
||||
@@ -517,10 +534,48 @@ static void registerAllComponents()
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
} lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "slotSelections"), lua_istable(L, -1)) {
|
||||
c.slotSelections.clear();
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_istable(L, -1)) {
|
||||
SlotSelection sel;
|
||||
Ogre::String slotName = lua_tostring(L, -2);
|
||||
if (lua_getfield(L, -1, "layer1Mesh"), lua_isstring(L, -1))
|
||||
sel.layer1Mesh = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1, "layer2Mesh"), lua_isstring(L, -1))
|
||||
sel.layer2Mesh = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1, "explicitMesh"), lua_isstring(L, -1))
|
||||
sel.explicitMesh = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
c.slotSelections[slotName] = sel;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
} lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "frontAxis"), lua_istable(L, -1))
|
||||
c.frontAxis = readVector3(L, lua_gettop(L));
|
||||
lua_pop(L, 1););
|
||||
|
||||
// --- CharacterShapeKeys ---
|
||||
REGISTER_COMPONENT(
|
||||
CharacterShapeKeysComponent, "CharacterShapeKeys",
|
||||
lua_newtable(L);
|
||||
for (auto &kv : c.weights) {
|
||||
lua_pushnumber(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
, if (lua_istable(L, idx)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, idx) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isnumber(L, -1))
|
||||
c.weights[lua_tostring(L, -2)] = lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// --- AnimationTree ---
|
||||
REGISTER_COMPONENT(
|
||||
AnimationTreeComponent, "AnimationTree",
|
||||
@@ -1245,52 +1300,6 @@ static void registerAllComponents()
|
||||
c.showQuit = lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1););
|
||||
|
||||
// --- Dialogue ---
|
||||
REGISTER_COMPONENT(
|
||||
DialogueComponent, "Dialogue",
|
||||
lua_pushstring(L, c.text.c_str());
|
||||
lua_setfield(L, -2, "text");
|
||||
lua_pushstring(L, c.speaker.c_str());
|
||||
lua_setfield(L, -2, "speaker"); pushStringVector(L, c.choices);
|
||||
lua_setfield(L, -2, "choices");
|
||||
lua_pushstring(L, c.fontName.c_str());
|
||||
lua_setfield(L, -2, "fontName"); lua_pushnumber(L, c.fontSize);
|
||||
lua_setfield(L, -2, "fontSize");
|
||||
lua_pushnumber(L, c.backgroundOpacity);
|
||||
lua_setfield(L, -2, "backgroundOpacity");
|
||||
lua_pushnumber(L, c.boxHeightFraction);
|
||||
lua_setfield(L, -2, "boxHeightFraction");
|
||||
lua_pushnumber(L, c.boxPositionFraction);
|
||||
lua_setfield(L, -2, "boxPositionFraction");
|
||||
lua_pushboolean(L, c.enabled ? 1 : 0);
|
||||
lua_setfield(L, -2, "enabled");
|
||||
, if (lua_getfield(L, idx, "text"), lua_isstring(L, -1))
|
||||
c.text = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "speaker"), lua_isstring(L, -1))
|
||||
c.speaker = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "choices"), lua_istable(L, -1))
|
||||
c.choices = readStringVector(L, lua_gettop(L));
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "fontName"), lua_isstring(L, -1))
|
||||
c.fontName = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "fontSize"), lua_isnumber(L, -1))
|
||||
c.fontSize = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1); if (lua_getfield(L, idx, "backgroundOpacity"),
|
||||
lua_isnumber(L, -1)) c.backgroundOpacity =
|
||||
(float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1); if (lua_getfield(L, idx, "boxHeightFraction"),
|
||||
lua_isnumber(L, -1)) c.boxHeightFraction =
|
||||
(float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1); if (lua_getfield(L, idx, "boxPositionFraction"),
|
||||
lua_isnumber(L, -1)) c.boxPositionFraction =
|
||||
(float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
|
||||
c.enabled = lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1););
|
||||
|
||||
// --- PlayerController ---
|
||||
REGISTER_COMPONENT(
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
#include "LuaDialogueApi.hpp"
|
||||
#include "../systems/DialogueSystem.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: read string vector from Lua table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static std::vector<Ogre::String> readStringVector(lua_State *L, int idx)
|
||||
{
|
||||
std::vector<Ogre::String> result;
|
||||
if (!lua_istable(L, idx))
|
||||
return result;
|
||||
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, idx) != 0) {
|
||||
if (lua_isstring(L, -1))
|
||||
result.push_back(lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua CFunctions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaDialogueShow(lua_State *L)
|
||||
{
|
||||
Ogre::String text;
|
||||
std::vector<Ogre::String> choices;
|
||||
Ogre::String speaker;
|
||||
|
||||
if (lua_gettop(L) >= 1 && lua_isstring(L, 1))
|
||||
text = lua_tostring(L, 1);
|
||||
|
||||
if (lua_gettop(L) >= 2 && lua_istable(L, 2))
|
||||
choices = readStringVector(L, 2);
|
||||
|
||||
if (lua_gettop(L) >= 3 && lua_isstring(L, 3))
|
||||
speaker = lua_tostring(L, 3);
|
||||
|
||||
DialogueSystem::getInstance().show(text, choices, speaker);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int luaDialogueHide(lua_State *L)
|
||||
{
|
||||
(void)L;
|
||||
DialogueSystem::getInstance().hide();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int luaDialogueIsActive(lua_State *L)
|
||||
{
|
||||
lua_pushboolean(L, DialogueSystem::getInstance().isActive() ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaDialogueSelectChoice(lua_State *L)
|
||||
{
|
||||
int index = 0;
|
||||
if (lua_gettop(L) >= 1 && lua_isnumber(L, 1))
|
||||
index = (int)lua_tonumber(L, 1);
|
||||
DialogueSystem::getInstance().selectChoice(index);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int luaDialogueProgress(lua_State *L)
|
||||
{
|
||||
(void)L;
|
||||
DialogueSystem::getInstance().progress();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int luaDialogueGetSettings(lua_State *L)
|
||||
{
|
||||
const auto &s = DialogueSystem::getInstance().getSettings();
|
||||
lua_newtable(L);
|
||||
lua_pushstring(L, s.fontName.c_str());
|
||||
lua_setfield(L, -2, "font_name");
|
||||
lua_pushnumber(L, s.fontSize);
|
||||
lua_setfield(L, -2, "font_size");
|
||||
lua_pushnumber(L, s.speakerFontSize);
|
||||
lua_setfield(L, -2, "speaker_font_size");
|
||||
lua_pushnumber(L, s.backgroundOpacity);
|
||||
lua_setfield(L, -2, "background_opacity");
|
||||
lua_pushnumber(L, s.boxHeightFraction);
|
||||
lua_setfield(L, -2, "box_height_fraction");
|
||||
lua_pushnumber(L, s.boxPositionFraction);
|
||||
lua_setfield(L, -2, "box_position_fraction");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaDialogueSetSettings(lua_State *L)
|
||||
{
|
||||
if (!lua_istable(L, 1))
|
||||
return 0;
|
||||
|
||||
auto s = DialogueSystem::getInstance().getSettings();
|
||||
|
||||
if (lua_getfield(L, 1, "font_name"), lua_isstring(L, -1))
|
||||
s.fontName = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (lua_getfield(L, 1, "font_size"), lua_isnumber(L, -1))
|
||||
s.fontSize = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (lua_getfield(L, 1, "speaker_font_size"), lua_isnumber(L, -1))
|
||||
s.speakerFontSize = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (lua_getfield(L, 1, "background_opacity"), lua_isnumber(L, -1))
|
||||
s.backgroundOpacity = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (lua_getfield(L, 1, "box_height_fraction"), lua_isnumber(L, -1))
|
||||
s.boxHeightFraction = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (lua_getfield(L, 1, "box_position_fraction"), lua_isnumber(L, -1))
|
||||
s.boxPositionFraction = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
DialogueSystem::getInstance().setSettings(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int luaDialogueSaveSettings(lua_State *L)
|
||||
{
|
||||
const char *path = "dialogue.json";
|
||||
if (lua_gettop(L) >= 1 && lua_isstring(L, 1))
|
||||
path = lua_tostring(L, 1);
|
||||
bool ok = DialogueSystem::getInstance().saveSettings(path);
|
||||
lua_pushboolean(L, ok ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaDialogueLoadSettings(lua_State *L)
|
||||
{
|
||||
const char *path = "dialogue.json";
|
||||
if (lua_gettop(L) >= 1 && lua_isstring(L, 1))
|
||||
path = lua_tostring(L, 1);
|
||||
bool ok = DialogueSystem::getInstance().loadSettings(path);
|
||||
lua_pushboolean(L, ok ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaDialogueApi(lua_State *L)
|
||||
{
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
}
|
||||
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushcfunction(L, luaDialogueShow);
|
||||
lua_setfield(L, -2, "show");
|
||||
|
||||
lua_pushcfunction(L, luaDialogueHide);
|
||||
lua_setfield(L, -2, "hide");
|
||||
|
||||
lua_pushcfunction(L, luaDialogueIsActive);
|
||||
lua_setfield(L, -2, "is_active");
|
||||
|
||||
lua_pushcfunction(L, luaDialogueSelectChoice);
|
||||
lua_setfield(L, -2, "select_choice");
|
||||
|
||||
lua_pushcfunction(L, luaDialogueProgress);
|
||||
lua_setfield(L, -2, "progress");
|
||||
|
||||
lua_pushcfunction(L, luaDialogueGetSettings);
|
||||
lua_setfield(L, -2, "get_settings");
|
||||
|
||||
lua_pushcfunction(L, luaDialogueSetSettings);
|
||||
lua_setfield(L, -2, "set_settings");
|
||||
|
||||
lua_pushcfunction(L, luaDialogueSaveSettings);
|
||||
lua_setfield(L, -2, "save_settings");
|
||||
|
||||
lua_pushcfunction(L, luaDialogueLoadSettings);
|
||||
lua_setfield(L, -2, "load_settings");
|
||||
|
||||
lua_setfield(L, -2, "dialogue");
|
||||
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef EDITSCENE_LUA_DIALOGUE_API_HPP
|
||||
#define EDITSCENE_LUA_DIALOGUE_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
void registerLuaDialogueApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_DIALOGUE_API_HPP
|
||||
@@ -0,0 +1,554 @@
|
||||
#include "CharacterClassSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "../components/CharacterClassComponent.hpp"
|
||||
#include "../components/CharacterClassDatabase.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/PlayerController.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
|
||||
CharacterClassSystem::CharacterClassSystem(flecs::world &world,
|
||||
EditorApp *editorApp)
|
||||
: m_world(world)
|
||||
, m_editorApp(editorApp)
|
||||
{
|
||||
}
|
||||
|
||||
CharacterClassSystem::~CharacterClassSystem()
|
||||
{
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool CharacterClassSystem::addXP(flecs::entity entity, int64_t amount)
|
||||
{
|
||||
if (!entity.is_alive() || !entity.has<CharacterClassComponent>())
|
||||
return false;
|
||||
|
||||
auto &cc = entity.get_mut<CharacterClassComponent>();
|
||||
cc.currentXP += amount;
|
||||
|
||||
const auto *cls = CharacterClassDatabase::getSingleton().findClass(
|
||||
cc.className);
|
||||
if (!cls)
|
||||
return false;
|
||||
|
||||
int64_t needed = CharacterClassDatabase::getSingleton()
|
||||
.computeXPForLevel(cc.level, *cls);
|
||||
if (cc.currentXP >= needed) {
|
||||
applyLevelUp(entity);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CharacterClassSystem::distributePoint(flecs::entity entity,
|
||||
const Ogre::String &statName)
|
||||
{
|
||||
if (!entity.is_alive() || !entity.has<CharacterClassComponent>())
|
||||
return false;
|
||||
|
||||
auto &cc = entity.get_mut<CharacterClassComponent>();
|
||||
if (cc.availablePoints <= 0)
|
||||
return false;
|
||||
|
||||
const auto *cls = CharacterClassDatabase::getSingleton().findClass(
|
||||
cc.className);
|
||||
if (!cls)
|
||||
return false;
|
||||
|
||||
const auto *statDef = CharacterClassDatabase::getSingleton().findStat(
|
||||
statName);
|
||||
if (!statDef)
|
||||
return false;
|
||||
|
||||
int current = cc.getStat(statName);
|
||||
int cost = CharacterClassDatabase::getSingleton().computeStatCost(
|
||||
current, *cls);
|
||||
if (cc.availablePoints < cost)
|
||||
return false;
|
||||
|
||||
cc.availablePoints -= cost;
|
||||
cc.stats[statName] = 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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CharacterClassSystem::update(float deltaTime)
|
||||
{
|
||||
accumulateNeeds(deltaTime);
|
||||
checkLevelUps();
|
||||
}
|
||||
|
||||
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);
|
||||
if (!cls)
|
||||
return;
|
||||
|
||||
for (auto &pair : cc.needs) {
|
||||
const auto *needDef = db.findNeed(pair.first);
|
||||
if (!needDef)
|
||||
continue;
|
||||
|
||||
pair.second += static_cast<int>(needDef->accumulationRate *
|
||||
deltaTime);
|
||||
if (pair.second > needDef->maxValue)
|
||||
pair.second = needDef->maxValue;
|
||||
if (pair.second < 0)
|
||||
pair.second = 0;
|
||||
}
|
||||
|
||||
updateNeedBits(e);
|
||||
});
|
||||
}
|
||||
|
||||
void CharacterClassSystem::updateNeedBits(flecs::entity entity)
|
||||
{
|
||||
if (!entity.has<CharacterClassComponent>())
|
||||
return;
|
||||
|
||||
auto &cc = entity.get_mut<CharacterClassComponent>();
|
||||
auto &db = CharacterClassDatabase::getSingleton();
|
||||
|
||||
if (!entity.has<GoapBlackboard>())
|
||||
entity.set<GoapBlackboard>({});
|
||||
|
||||
auto &bb = entity.get_mut<GoapBlackboard>();
|
||||
|
||||
for (const auto &pair : cc.needs) {
|
||||
const auto *needDef = db.findNeed(pair.first);
|
||||
if (!needDef || needDef->bitName.empty())
|
||||
continue;
|
||||
|
||||
int bitIdx = GoapBlackboard::findBitByName(needDef->bitName);
|
||||
if (bitIdx < 0)
|
||||
continue;
|
||||
|
||||
int value = pair.second;
|
||||
bool currentlySet = bb.getBit(bitIdx);
|
||||
|
||||
// Hysteresis: set at high threshold, clear at low threshold
|
||||
if (value >= needDef->highThreshold)
|
||||
bb.setBit(bitIdx, true);
|
||||
else if (value <= needDef->lowThreshold)
|
||||
bb.setBit(bitIdx, false);
|
||||
// else: keep current state (dead zone between thresholds)
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterClassSystem::checkLevelUps()
|
||||
{
|
||||
m_world.query<CharacterClassComponent>().each(
|
||||
[&](flecs::entity e, CharacterClassComponent &cc) {
|
||||
if (cc.levelUpPending)
|
||||
return;
|
||||
|
||||
const auto *cls = CharacterClassDatabase::getSingleton()
|
||||
.findClass(cc.className);
|
||||
if (!cls)
|
||||
return;
|
||||
|
||||
int64_t needed = CharacterClassDatabase::getSingleton()
|
||||
.computeXPForLevel(cc.level, *cls);
|
||||
if (cc.currentXP >= needed) {
|
||||
applyLevelUp(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CharacterClassSystem::applyLevelUp(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;
|
||||
|
||||
int64_t needed = CharacterClassDatabase::getSingleton().computeXPForLevel(
|
||||
cc.level, *cls);
|
||||
cc.currentXP -= needed;
|
||||
cc.level++;
|
||||
|
||||
applyLevelUpGrowthAndPoints(entity);
|
||||
|
||||
// Check if player
|
||||
bool isPlayer = entity.has<PlayerControllerComponent>();
|
||||
if (isPlayer && m_editorApp &&
|
||||
m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
|
||||
m_editorApp->getGamePlayState() ==
|
||||
EditorApp::GamePlayState::Playing) {
|
||||
cc.levelUpPending = true;
|
||||
m_levelUpDialogs.insert(entity.id());
|
||||
} else {
|
||||
// AI: auto-distribute immediately
|
||||
distributePointsAI(entity);
|
||||
}
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
Ogre::String("CharacterClassSystem: ") +
|
||||
Ogre::String(entity.name()) +
|
||||
" reached level " + std::to_string(cc.level) + "!");
|
||||
}
|
||||
|
||||
void CharacterClassSystem::distributePointsAI(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 points = cc.availablePoints;
|
||||
|
||||
// Round-robin primary stats
|
||||
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 = cc.getStat(statName);
|
||||
int cost = CharacterClassDatabase::getSingleton()
|
||||
.computeStatCost(current, *cls);
|
||||
if (points >= cost) {
|
||||
cc.stats[statName] = current + 1;
|
||||
points -= cost;
|
||||
idx++;
|
||||
} else {
|
||||
idx++;
|
||||
if (idx >= (int)cls->primaryStats.size() * 2)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Random distribution for remainder
|
||||
if (points > 0 && !cc.stats.empty()) {
|
||||
std::vector<Ogre::String> statNames;
|
||||
for (const auto &pair : cc.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 = cc.getStat(statName);
|
||||
int cost = CharacterClassDatabase::getSingleton()
|
||||
.computeStatCost(current, *cls);
|
||||
if (points >= cost) {
|
||||
cc.stats[statName] = current + 1;
|
||||
points -= cost;
|
||||
} else {
|
||||
// Can't afford this stat, remove from pool
|
||||
statNames.erase(statNames.begin() + r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cc.availablePoints = points;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dialog rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CharacterClassSystem::renderDialogs()
|
||||
{
|
||||
// Level-up dialogs
|
||||
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>()) {
|
||||
closedDialogs.push_back(id);
|
||||
continue;
|
||||
}
|
||||
renderLevelUpDialog(e);
|
||||
}
|
||||
for (flecs::entity_t id : closedDialogs)
|
||||
m_levelUpDialogs.erase(id);
|
||||
|
||||
// Character sheets
|
||||
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>()) {
|
||||
closedSheets.push_back(id);
|
||||
continue;
|
||||
}
|
||||
renderCharacterSheet(e);
|
||||
}
|
||||
for (flecs::entity_t id : closedSheets)
|
||||
m_sheets.erase(id);
|
||||
}
|
||||
|
||||
void CharacterClassSystem::renderLevelUpDialog(flecs::entity entity)
|
||||
{
|
||||
if (!entity.has<CharacterClassComponent>())
|
||||
return;
|
||||
|
||||
auto &cc = entity.get_mut<CharacterClassComponent>();
|
||||
const auto *cls = CharacterClassDatabase::getSingleton().findClass(
|
||||
cc.className);
|
||||
if (!cls)
|
||||
return;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(200, 200), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(450, 500),
|
||||
ImGuiCond_FirstUseEver);
|
||||
|
||||
Ogre::String title = "Level Up! (Level " +
|
||||
std::to_string(cc.level) + ")";
|
||||
bool open = true;
|
||||
if (!ImGui::Begin(title.c_str(), &open)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Text("Available Points: %d", cc.availablePoints);
|
||||
ImGui::Separator();
|
||||
|
||||
auto &db = CharacterClassDatabase::getSingleton();
|
||||
|
||||
// Stats
|
||||
if (!cc.stats.empty()) {
|
||||
ImGui::Text("Stats");
|
||||
for (auto &pair : cc.stats) {
|
||||
const auto *statDef = db.findStat(pair.first);
|
||||
int current = pair.second;
|
||||
int cost = db.computeStatCost(current, *cls);
|
||||
bool canAfford = cc.availablePoints >= cost;
|
||||
|
||||
ImGui::PushID(pair.first.c_str());
|
||||
ImGui::Text("%s: %d", pair.first.c_str(), current);
|
||||
ImGui::SameLine(150);
|
||||
ImGui::Text("Cost: %d", cost);
|
||||
ImGui::SameLine(220);
|
||||
if (ImGui::Button("+", ImVec2(30, 0)) && canAfford) {
|
||||
cc.availablePoints -= cost;
|
||||
pair.second = current + 1;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Button("Confirm", ImVec2(100, 0))) {
|
||||
cc.levelUpPending = false;
|
||||
open = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Postpone", ImVec2(100, 0))) {
|
||||
open = false;
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
if (!open) {
|
||||
m_levelUpDialogs.erase(entity.id());
|
||||
cc.levelUpPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterClassSystem::renderCharacterSheet(flecs::entity entity)
|
||||
{
|
||||
if (!entity.has<CharacterClassComponent>())
|
||||
return;
|
||||
|
||||
auto &cc = entity.get_mut<CharacterClassComponent>();
|
||||
const auto *cls = CharacterClassDatabase::getSingleton().findClass(
|
||||
cc.className);
|
||||
|
||||
Ogre::String title = "Character Sheet";
|
||||
bool open = true;
|
||||
ImGui::SetNextWindowPos(ImVec2(250, 150), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(400, 500),
|
||||
ImGuiCond_FirstUseEver);
|
||||
|
||||
if (!ImGui::Begin(title.c_str(), &open)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
ImGui::Separator();
|
||||
|
||||
// Stats
|
||||
if (!cc.stats.empty()) {
|
||||
ImGui::Text("Stats");
|
||||
for (const auto &pair : cc.stats) {
|
||||
ImGui::Text(" %s: %d", pair.first.c_str(),
|
||||
pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (!cc.skills.empty()) {
|
||||
ImGui::Text("Skills");
|
||||
for (const auto &pair : cc.skills) {
|
||||
ImGui::Text(" %s: %d", pair.first.c_str(),
|
||||
pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
// Needs
|
||||
if (!cc.needs.empty()) {
|
||||
ImGui::Text("Needs");
|
||||
for (const auto &pair : cc.needs) {
|
||||
ImGui::Text(" %s: %d", pair.first.c_str(),
|
||||
pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Inventory placeholder
|
||||
if (entity.has<InventoryComponent>()) {
|
||||
ImGui::Text("Inventory available (not yet displayed)");
|
||||
} else {
|
||||
ImGui::Text("No inventory");
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
if (!open)
|
||||
m_sheets.erase(entity.id());
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#ifndef EDITSCENE_CHARACTER_CLASS_SYSTEM_HPP
|
||||
#define EDITSCENE_CHARACTER_CLASS_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <unordered_set>
|
||||
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
* System that manages character progression, need accumulation,
|
||||
* and level-up logic.
|
||||
*
|
||||
* - Accumulates needs each frame based on class database rates.
|
||||
* - Sets/clears GOAP blackboard bits when needs cross thresholds.
|
||||
* - Checks XP and triggers level-ups.
|
||||
* - AI entities auto-distribute stat points.
|
||||
* - Player entities get a level-up dialog.
|
||||
*/
|
||||
class CharacterClassSystem {
|
||||
public:
|
||||
CharacterClassSystem(flecs::world &world, EditorApp *editorApp);
|
||||
~CharacterClassSystem();
|
||||
|
||||
/** Call every frame. Handles need ticks, level-up checks, dialog. */
|
||||
void update(float deltaTime);
|
||||
|
||||
/** Render level-up and character-sheet dialogs (inside ImGui frame). */
|
||||
void renderDialogs();
|
||||
|
||||
/** Manually add XP to an entity. Returns true if a level up occurred. */
|
||||
bool addXP(flecs::entity entity, int64_t amount);
|
||||
|
||||
/** 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 renderLevelUpDialog(flecs::entity entity);
|
||||
void renderCharacterSheet(flecs::entity entity);
|
||||
|
||||
flecs::world &m_world;
|
||||
EditorApp *m_editorApp;
|
||||
|
||||
// Track which entities have an open level-up dialog
|
||||
std::unordered_set<flecs::entity_t> m_levelUpDialogs;
|
||||
// Track which entities have an open character sheet
|
||||
std::unordered_set<flecs::entity_t> m_sheets;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTER_CLASS_SYSTEM_HPP
|
||||
@@ -1,12 +1,17 @@
|
||||
#include "CharacterSlotSystem.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/AnimationTree.hpp"
|
||||
#include <OgreAnimation.h>
|
||||
#include <OgreAnimationState.h>
|
||||
#include <OgreAnimationTrack.h>
|
||||
#include <OgreDataStream.h>
|
||||
#include <OgreEntity.h>
|
||||
#include <OgreKeyFrame.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreMesh.h>
|
||||
#include <OgreResourceGroupManager.h>
|
||||
#include <OgreSceneNode.h>
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
|
||||
bool CharacterSlotSystem::s_catalogLoaded = false;
|
||||
nlohmann::json CharacterSlotSystem::s_bodyParts = nlohmann::json::object();
|
||||
@@ -62,6 +67,7 @@ void CharacterSlotSystem::loadCatalog()
|
||||
names->end());
|
||||
}
|
||||
|
||||
|
||||
for (const auto &name : partNames) {
|
||||
Ogre::String group = rgm.findGroupContainingResource(name);
|
||||
Ogre::DataStreamPtr stream = rgm.openResource(name, group);
|
||||
@@ -84,7 +90,8 @@ void CharacterSlotSystem::loadCatalog()
|
||||
if (!s_bodyParts[age][sex].contains(slot))
|
||||
s_bodyParts[age][sex][slot] =
|
||||
nlohmann::json::array();
|
||||
s_bodyParts[age][sex][slot].push_back(mesh);
|
||||
|
||||
s_bodyParts[age][sex][slot].push_back(jdata);
|
||||
s_meshNames.insert(mesh);
|
||||
|
||||
/* Preload mesh into Characters group */
|
||||
@@ -98,6 +105,7 @@ void CharacterSlotSystem::loadCatalog()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
s_catalogLoaded = true;
|
||||
}
|
||||
|
||||
@@ -139,6 +147,51 @@ std::vector<Ogre::String> CharacterSlotSystem::getSlots(
|
||||
return slots;
|
||||
}
|
||||
|
||||
std::vector<Ogre::String> CharacterSlotSystem::getMeshesForLayer(
|
||||
const Ogre::String &age, const Ogre::String &sex,
|
||||
const Ogre::String &slot, int layer)
|
||||
{
|
||||
std::vector<Ogre::String> meshes;
|
||||
if (!s_catalogLoaded || !s_bodyParts.contains(age) ||
|
||||
!s_bodyParts[age].contains(sex) ||
|
||||
!s_bodyParts[age][sex].contains(slot))
|
||||
return meshes;
|
||||
|
||||
for (const auto &entry : s_bodyParts[age][sex][slot]) {
|
||||
int entryLayer = entry.value("layer", 0);
|
||||
if (entryLayer == layer)
|
||||
meshes.push_back(entry.value("mesh", ""));
|
||||
}
|
||||
return meshes;
|
||||
}
|
||||
|
||||
Ogre::String CharacterSlotSystem::getMeshLabel(
|
||||
const Ogre::String &age, const Ogre::String &sex,
|
||||
const Ogre::String &slot, const Ogre::String &mesh)
|
||||
{
|
||||
if (!s_catalogLoaded || !s_bodyParts.contains(age) ||
|
||||
!s_bodyParts[age].contains(sex) ||
|
||||
!s_bodyParts[age][sex].contains(slot))
|
||||
return mesh;
|
||||
|
||||
for (const auto &entry : s_bodyParts[age][sex][slot]) {
|
||||
if (entry.value("mesh", "") == mesh) {
|
||||
const auto &garments =
|
||||
entry.value("garments", nlohmann::json::array());
|
||||
if (garments.empty())
|
||||
return "nude";
|
||||
Ogre::String label;
|
||||
for (size_t i = 0; i < garments.size(); ++i) {
|
||||
if (i > 0)
|
||||
label += " + ";
|
||||
label += garments[i].get<std::string>();
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return mesh;
|
||||
}
|
||||
|
||||
std::vector<Ogre::String> CharacterSlotSystem::getMeshes(
|
||||
const Ogre::String &age, const Ogre::String &sex,
|
||||
const Ogre::String &slot)
|
||||
@@ -148,55 +201,188 @@ std::vector<Ogre::String> CharacterSlotSystem::getMeshes(
|
||||
!s_bodyParts[age].contains(sex) ||
|
||||
!s_bodyParts[age][sex].contains(slot))
|
||||
return meshes;
|
||||
for (auto &m : s_bodyParts[age][sex][slot])
|
||||
meshes.push_back(m.get<Ogre::String>());
|
||||
for (const auto &entry : s_bodyParts[age][sex][slot])
|
||||
meshes.push_back(entry["mesh"].get<Ogre::String>());
|
||||
return meshes;
|
||||
}
|
||||
|
||||
std::vector<Ogre::String> CharacterSlotSystem::getShapeKeyNames(
|
||||
const Ogre::String &age, const Ogre::String &sex)
|
||||
{
|
||||
std::set<Ogre::String> keySet;
|
||||
if (!s_catalogLoaded || !s_bodyParts.contains(age) ||
|
||||
!s_bodyParts[age].contains(sex))
|
||||
return {};
|
||||
|
||||
for (auto &slotEl : s_bodyParts[age][sex].items()) {
|
||||
for (const auto &entry : slotEl.value()) {
|
||||
const auto &keys =
|
||||
entry.value("shape_keys", nlohmann::json::array());
|
||||
for (const auto &k : keys)
|
||||
keySet.insert(k.get<Ogre::String>());
|
||||
}
|
||||
}
|
||||
|
||||
return std::vector<Ogre::String>(keySet.begin(), keySet.end());
|
||||
}
|
||||
|
||||
Ogre::String CharacterSlotSystem::resolveMesh(
|
||||
const Ogre::String &age, const Ogre::String &sex,
|
||||
const Ogre::String &slot, const SlotSelection &sel,
|
||||
int outfitLevel)
|
||||
{
|
||||
if (!sel.explicitMesh.empty())
|
||||
return sel.explicitMesh;
|
||||
|
||||
if (!s_catalogLoaded || !s_bodyParts.contains(age) ||
|
||||
!s_bodyParts[age].contains(sex) ||
|
||||
!s_bodyParts[age][sex].contains(slot))
|
||||
return "";
|
||||
|
||||
const auto &slotEntries = s_bodyParts[age][sex][slot];
|
||||
|
||||
/* outfitLevel: 0=nude, 1=lingerie, 2=clothed */
|
||||
if (outfitLevel >= 2 && sel.layer2Mesh != "none" &&
|
||||
!sel.layer2Mesh.empty()) {
|
||||
for (const auto &entry : slotEntries) {
|
||||
if (entry.value("mesh", "") == sel.layer2Mesh)
|
||||
return sel.layer2Mesh;
|
||||
}
|
||||
}
|
||||
if (outfitLevel >= 1 && sel.layer1Mesh != "none" &&
|
||||
!sel.layer1Mesh.empty()) {
|
||||
for (const auto &entry : slotEntries) {
|
||||
if (entry.value("mesh", "") == sel.layer1Mesh)
|
||||
return sel.layer1Mesh;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback to layer 0 (nude base) — prefer canonical base mesh */
|
||||
Ogre::String canonicalBase = sex + "_" + slot + ".mesh";
|
||||
Ogre::String firstLayer0;
|
||||
for (const auto &entry : slotEntries) {
|
||||
if (entry.value("layer", 0) == 0) {
|
||||
Ogre::String mesh = entry["mesh"].get<Ogre::String>();
|
||||
if (mesh == canonicalBase)
|
||||
return mesh;
|
||||
if (firstLayer0.empty())
|
||||
firstLayer0 = mesh;
|
||||
}
|
||||
}
|
||||
if (!firstLayer0.empty())
|
||||
return firstLayer0;
|
||||
|
||||
/* Last resort: first available entry */
|
||||
if (!slotEntries.empty())
|
||||
return slotEntries[0]["mesh"].get<Ogre::String>();
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
void CharacterSlotSystem::update()
|
||||
{
|
||||
if (!m_initialized)
|
||||
return;
|
||||
|
||||
int total = 0;
|
||||
int dirtyCount = 0;
|
||||
m_world.query<CharacterSlotsComponent>().each(
|
||||
[this](flecs::entity e, CharacterSlotsComponent &cs) {
|
||||
[&](flecs::entity e, CharacterSlotsComponent &cs) {
|
||||
total++;
|
||||
if (cs.dirty) {
|
||||
std::cout << "CharacterSlotSystem: building entity "
|
||||
<< e.id() << std::endl;
|
||||
dirtyCount++;
|
||||
buildCharacter(e, cs);
|
||||
cs.dirty = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static const nlohmann::json *findCatalogEntry(
|
||||
const Ogre::String &age, const Ogre::String &sex,
|
||||
const Ogre::String &slot, const Ogre::String &mesh)
|
||||
{
|
||||
if (!CharacterSlotSystem::isCatalogLoaded())
|
||||
return nullptr;
|
||||
const nlohmann::json &cat = CharacterSlotSystem::getCatalog();
|
||||
if (!cat.contains(age) || !cat[age].contains(sex) ||
|
||||
!cat[age][sex].contains(slot))
|
||||
return nullptr;
|
||||
for (const auto &entry : cat[age][sex][slot]) {
|
||||
if (entry.value("mesh", "") == mesh)
|
||||
return &entry;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void ensureMeshPoseAnimation(const Ogre::String &meshName)
|
||||
{
|
||||
Ogre::MeshPtr mesh;
|
||||
try {
|
||||
mesh = Ogre::MeshManager::getSingleton().load(meshName,
|
||||
"Characters");
|
||||
} catch (...) {
|
||||
return;
|
||||
}
|
||||
if (!mesh || mesh->getPoseCount() == 0)
|
||||
return;
|
||||
try {
|
||||
mesh->getAnimation("ShapeKeys");
|
||||
} catch (...) {
|
||||
Ogre::Animation *anim = mesh->createAnimation("ShapeKeys", 1.0f);
|
||||
Ogre::VertexAnimationTrack *track = anim->createVertexTrack(
|
||||
0, Ogre::VAT_POSE);
|
||||
Ogre::VertexPoseKeyFrame *kf =
|
||||
track->createVertexPoseKeyFrame(0.0f);
|
||||
for (size_t i = 0; i < mesh->getPoseCount(); ++i)
|
||||
kf->addPoseReference(static_cast<ushort>(i), 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
CharacterSlotsComponent &cs)
|
||||
{
|
||||
std::cout << "CharacterSlotSystem::buildCharacter: entity=" << e.id()
|
||||
<< " age=" << cs.age << " sex=" << cs.sex
|
||||
<< " slots=" << cs.slots.size() << std::endl;
|
||||
|
||||
destroyCharacterParts(e);
|
||||
|
||||
if (!e.has<TransformComponent>()) {
|
||||
std::cout << " no TransformComponent" << std::endl;
|
||||
return;
|
||||
/* Migrate old slots map to slotSelections if needed */
|
||||
if (cs.slotSelections.empty() && !cs.slots.empty()) {
|
||||
for (const auto &pair : cs.slots) {
|
||||
SlotSelection sel;
|
||||
sel.explicitMesh = pair.second;
|
||||
cs.slotSelections[pair.first] = sel;
|
||||
}
|
||||
}
|
||||
|
||||
auto &transform = e.get_mut<TransformComponent>();
|
||||
if (!transform.node) {
|
||||
std::cout << " transform.node is null" << std::endl;
|
||||
return;
|
||||
/* Populate default slots from catalog if still empty */
|
||||
if (cs.slotSelections.empty()) {
|
||||
auto slots = getSlots(cs.age, cs.sex);
|
||||
for (const auto &slot : slots) {
|
||||
SlotSelection sel;
|
||||
cs.slotSelections[slot] = sel;
|
||||
}
|
||||
}
|
||||
|
||||
if (!e.has<TransformComponent>())
|
||||
return;
|
||||
|
||||
auto &transform = e.get_mut<TransformComponent>();
|
||||
if (!transform.node)
|
||||
return;
|
||||
|
||||
/* Determine master slot (face preferred, else first non-empty) */
|
||||
Ogre::String masterSlot;
|
||||
if (cs.slots.find("face") != cs.slots.end() &&
|
||||
!cs.slots.at("face").empty()) {
|
||||
masterSlot = "face";
|
||||
} else {
|
||||
for (const auto &pair : cs.slots) {
|
||||
if (!pair.second.empty()) {
|
||||
if (cs.slotSelections.find("face") != cs.slotSelections.end()) {
|
||||
Ogre::String mesh = resolveMesh(cs.age, cs.sex, "face",
|
||||
cs.slotSelections["face"],
|
||||
cs.outfitLevel);
|
||||
if (!mesh.empty())
|
||||
masterSlot = "face";
|
||||
}
|
||||
if (masterSlot.empty()) {
|
||||
for (const auto &pair : cs.slotSelections) {
|
||||
Ogre::String mesh = resolveMesh(cs.age, cs.sex, pair.first,
|
||||
pair.second,
|
||||
cs.outfitLevel);
|
||||
if (!mesh.empty()) {
|
||||
masterSlot = pair.first;
|
||||
break;
|
||||
}
|
||||
@@ -204,55 +390,68 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
}
|
||||
|
||||
if (masterSlot.empty()) {
|
||||
std::cout << " masterSlot empty" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << " masterSlot=" << masterSlot
|
||||
<< " mesh=" << cs.slots.at(masterSlot) << std::endl;
|
||||
Ogre::String masterMesh = resolveMesh(cs.age, cs.sex, masterSlot,
|
||||
cs.slotSelections[masterSlot],
|
||||
cs.outfitLevel);
|
||||
|
||||
if (masterMesh.empty())
|
||||
return;
|
||||
|
||||
/* Pre-create pose animation on mesh so entity knows about it */
|
||||
ensureMeshPoseAnimation(masterMesh);
|
||||
|
||||
Ogre::Entity *masterEnt = nullptr;
|
||||
try {
|
||||
masterEnt = m_sceneMgr->createEntity(cs.slots.at(masterSlot));
|
||||
Ogre::MeshPtr meshPtr = Ogre::MeshManager::getSingleton().load(
|
||||
masterMesh, "Characters");
|
||||
masterEnt = m_sceneMgr->createEntity(meshPtr);
|
||||
transform.node->attachObject(masterEnt);
|
||||
m_entities[e.id()].parts[masterSlot] = masterEnt;
|
||||
cs.masterEntity = masterEnt;
|
||||
std::cout << " master loaded: " << masterEnt->getName()
|
||||
<< std::endl;
|
||||
std::cout << " node=" << transform.node->getName()
|
||||
<< " pos=" << transform.node->_getDerivedPosition()
|
||||
<< " attached=" << transform.node->numAttachedObjects()
|
||||
<< std::endl;
|
||||
|
||||
/* Setup pose animation for shape keys */
|
||||
const nlohmann::json *entry = findCatalogEntry(
|
||||
cs.age, cs.sex, masterSlot, masterMesh);
|
||||
applyShapeKeys(e, masterEnt, entry);
|
||||
|
||||
/* Notify AnimationTreeSystem that entity changed */
|
||||
if (e.has<AnimationTreeComponent>())
|
||||
e.get_mut<AnimationTreeComponent>().dirty = true;
|
||||
} catch (const Ogre::Exception &ex) {
|
||||
std::cout << " FAILED to load master mesh: "
|
||||
<< ex.getDescription() << std::endl;
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CharacterSlotSystem: Failed to load master mesh '" +
|
||||
cs.slots.at(masterSlot) + "': " + ex.getDescription());
|
||||
masterMesh + "': " + ex.getDescription());
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &pair : cs.slots) {
|
||||
for (const auto &pair : cs.slotSelections) {
|
||||
const Ogre::String &slot = pair.first;
|
||||
const Ogre::String &mesh = pair.second;
|
||||
const SlotSelection &sel = pair.second;
|
||||
|
||||
if (slot == masterSlot || mesh.empty())
|
||||
if (slot == masterSlot)
|
||||
continue;
|
||||
|
||||
Ogre::String mesh = resolveMesh(cs.age, cs.sex, slot, sel,
|
||||
cs.outfitLevel);
|
||||
if (mesh.empty())
|
||||
continue;
|
||||
|
||||
try {
|
||||
Ogre::Entity *partEnt = m_sceneMgr->createEntity(mesh);
|
||||
ensureMeshPoseAnimation(mesh);
|
||||
Ogre::MeshPtr partMesh =
|
||||
Ogre::MeshManager::getSingleton().load(
|
||||
mesh, "Characters");
|
||||
Ogre::Entity *partEnt = m_sceneMgr->createEntity(partMesh);
|
||||
partEnt->shareSkeletonInstanceWith(masterEnt);
|
||||
transform.node->attachObject(partEnt);
|
||||
m_entities[e.id()].parts[slot] = partEnt;
|
||||
std::cout << " part loaded: " << slot << "="
|
||||
<< partEnt->getName() << std::endl;
|
||||
const nlohmann::json *entry = findCatalogEntry(
|
||||
cs.age, cs.sex, slot, mesh);
|
||||
applyShapeKeys(e, partEnt, entry);
|
||||
} catch (const Ogre::Exception &ex) {
|
||||
std::cout << " FAILED to load part " << slot
|
||||
<< ": " << ex.getDescription() << std::endl;
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CharacterSlotSystem: Failed to load part '" +
|
||||
slot + "' mesh '" + mesh +
|
||||
@@ -261,6 +460,71 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterSlotSystem::applyShapeKeys(flecs::entity e,
|
||||
Ogre::Entity *ent,
|
||||
const nlohmann::json *entry)
|
||||
{
|
||||
if (!ent || !entry)
|
||||
return;
|
||||
|
||||
Ogre::MeshPtr mesh = ent->getMesh();
|
||||
if (!mesh || mesh->getPoseCount() == 0)
|
||||
return;
|
||||
|
||||
/* Create a pose animation track if one doesn't exist */
|
||||
Ogre::Animation *anim = nullptr;
|
||||
try {
|
||||
anim = mesh->getAnimation("ShapeKeys");
|
||||
} catch (...) {
|
||||
anim = mesh->createAnimation("ShapeKeys", 1.0f);
|
||||
Ogre::VertexAnimationTrack *track = anim->createVertexTrack(
|
||||
0, Ogre::VAT_POSE);
|
||||
Ogre::VertexPoseKeyFrame *kf =
|
||||
track->createVertexPoseKeyFrame(0.0f);
|
||||
for (size_t i = 0; i < mesh->getPoseCount(); ++i)
|
||||
kf->addPoseReference(static_cast<ushort>(i), 0.0f);
|
||||
}
|
||||
|
||||
if (!ent->hasAnimationState("ShapeKeys"))
|
||||
return;
|
||||
Ogre::AnimationState *as = ent->getAnimationState("ShapeKeys");
|
||||
as->setEnabled(true);
|
||||
as->setLoop(false);
|
||||
|
||||
/* Build name -> pose index map from catalog */
|
||||
const auto &shapeKeys =
|
||||
entry->value("shape_keys", nlohmann::json::array());
|
||||
std::unordered_map<Ogre::String, size_t> nameToIndex;
|
||||
for (size_t i = 0; i < shapeKeys.size(); ++i)
|
||||
nameToIndex[shapeKeys[i].get<Ogre::String>()] = i;
|
||||
|
||||
/* Apply weights from CharacterShapeKeysComponent */
|
||||
if (e.has<CharacterShapeKeysComponent>()) {
|
||||
auto &skc = e.get_mut<CharacterShapeKeysComponent>();
|
||||
for (const auto &pair : skc.weights) {
|
||||
auto it = nameToIndex.find(pair.first);
|
||||
if (it == nameToIndex.end())
|
||||
continue;
|
||||
if (it->second >= mesh->getPoseCount())
|
||||
continue;
|
||||
/* Update the keyframe's pose reference influence */
|
||||
Ogre::VertexAnimationTrack *track =
|
||||
anim->getVertexTrack(0);
|
||||
if (track) {
|
||||
Ogre::VertexPoseKeyFrame *kf =
|
||||
track->getVertexPoseKeyFrame(0);
|
||||
if (kf)
|
||||
kf->updatePoseReference(
|
||||
static_cast<ushort>(it->second),
|
||||
pair.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Force OGRE to update the entity with new pose weights */
|
||||
as->setTimePosition(0.0f);
|
||||
}
|
||||
|
||||
void CharacterSlotSystem::destroyCharacterParts(flecs::entity e)
|
||||
{
|
||||
auto it = m_entities.find(e.id());
|
||||
@@ -283,7 +547,7 @@ void CharacterSlotSystem::destroyCharacterParts(flecs::entity e)
|
||||
}
|
||||
|
||||
Ogre::Entity *CharacterSlotSystem::getSlotEntity(flecs::entity e,
|
||||
const Ogre::String &slot)
|
||||
const Ogre::String &slot)
|
||||
{
|
||||
auto it = m_entities.find(e.id());
|
||||
if (it == m_entities.end())
|
||||
@@ -295,8 +559,8 @@ Ogre::Entity *CharacterSlotSystem::getSlotEntity(flecs::entity e,
|
||||
}
|
||||
|
||||
void CharacterSlotSystem::setSlotVisible(flecs::entity e,
|
||||
const Ogre::String &slot,
|
||||
bool visible)
|
||||
const Ogre::String &slot,
|
||||
bool visible)
|
||||
{
|
||||
Ogre::Entity *ent = getSlotEntity(e, slot);
|
||||
if (ent) {
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
|
||||
/**
|
||||
* Rich catalog entry for a body part mesh.
|
||||
*/
|
||||
struct BodyPartEntry {
|
||||
Ogre::String mesh;
|
||||
int layer = 0;
|
||||
std::vector<Ogre::String> garments;
|
||||
std::vector<Ogre::String> tags;
|
||||
std::vector<Ogre::String> shapeKeys;
|
||||
};
|
||||
|
||||
/**
|
||||
* System that manages multi-slot character meshes with shared skeleton.
|
||||
* Loads body part catalog from body_part_*.json files and creates/updates
|
||||
@@ -31,10 +42,37 @@ public:
|
||||
static std::vector<Ogre::String> getSexes(const Ogre::String &age);
|
||||
static std::vector<Ogre::String> getSlots(const Ogre::String &age,
|
||||
const Ogre::String &sex);
|
||||
|
||||
/* Query meshes for a specific layer */
|
||||
static std::vector<Ogre::String> getMeshesForLayer(
|
||||
const Ogre::String &age, const Ogre::String &sex,
|
||||
const Ogre::String &slot, int layer);
|
||||
|
||||
/* Get display label for a catalog entry (garment names joined) */
|
||||
static Ogre::String getMeshLabel(const Ogre::String &age,
|
||||
const Ogre::String &sex,
|
||||
const Ogre::String &slot,
|
||||
const Ogre::String &mesh);
|
||||
|
||||
/* Legacy flat list (for editor explicit mesh fallback) */
|
||||
static std::vector<Ogre::String> getMeshes(const Ogre::String &age,
|
||||
const Ogre::String &sex,
|
||||
const Ogre::String &slot);
|
||||
|
||||
/* Raw catalog access for systems that need full metadata */
|
||||
static const nlohmann::json &getCatalog() { return s_bodyParts; }
|
||||
|
||||
/* Shape key vocabulary for UI */
|
||||
static std::vector<Ogre::String> getShapeKeyNames(
|
||||
const Ogre::String &age, const Ogre::String &sex);
|
||||
|
||||
/* Resolve a single slot to a mesh name given outfit level */
|
||||
static Ogre::String resolveMesh(const Ogre::String &age,
|
||||
const Ogre::String &sex,
|
||||
const Ogre::String &slot,
|
||||
const SlotSelection &sel,
|
||||
int outfitLevel);
|
||||
|
||||
/* Slot visibility helpers */
|
||||
Ogre::Entity *getSlotEntity(flecs::entity e,
|
||||
const Ogre::String &slot);
|
||||
@@ -48,6 +86,8 @@ private:
|
||||
|
||||
void buildCharacter(flecs::entity e, CharacterSlotsComponent &cs);
|
||||
void destroyCharacterParts(flecs::entity e);
|
||||
void applyShapeKeys(flecs::entity e, Ogre::Entity *ent,
|
||||
const nlohmann::json *entry);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
|
||||
@@ -1,182 +1,336 @@
|
||||
#include "DialogueSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "../components/DialogueComponent.hpp"
|
||||
#include "../systems/EventBus.hpp"
|
||||
#include "../components/EventParams.hpp"
|
||||
#include <imgui.h>
|
||||
#include <OgreFontManager.h>
|
||||
#include <OgreImGuiOverlay.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreOverlayManager.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
|
||||
DialogueSystem::DialogueSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_editorApp(editorApp)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings JSON
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool DialogueSystem::Settings::loadFromJson(const std::string &path)
|
||||
{
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open())
|
||||
return false;
|
||||
|
||||
nlohmann::json j;
|
||||
try {
|
||||
f >> j;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fontName = j.value("fontName", fontName);
|
||||
fontSize = j.value("fontSize", fontSize);
|
||||
speakerFontSize = j.value("speakerFontSize", speakerFontSize);
|
||||
backgroundOpacity = j.value("backgroundOpacity", backgroundOpacity);
|
||||
boxHeightFraction = j.value("boxHeightFraction", boxHeightFraction);
|
||||
boxPositionFraction = j.value("boxPositionFraction", boxPositionFraction);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DialogueSystem::Settings::saveToJson(const std::string &path) const
|
||||
{
|
||||
nlohmann::json j;
|
||||
j["fontName"] = fontName;
|
||||
j["fontSize"] = fontSize;
|
||||
j["speakerFontSize"] = speakerFontSize;
|
||||
j["backgroundOpacity"] = backgroundOpacity;
|
||||
j["boxHeightFraction"] = boxHeightFraction;
|
||||
j["boxPositionFraction"] = boxPositionFraction;
|
||||
|
||||
std::ofstream f(path);
|
||||
if (!f.is_open())
|
||||
return false;
|
||||
|
||||
try {
|
||||
f << j.dump(4);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Singleton
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
DialogueSystem::DialogueSystem()
|
||||
{
|
||||
// Subscribe to dialogue events
|
||||
EventBus::getInstance().subscribe(
|
||||
"dialogue_show", [this](const Ogre::String &,
|
||||
const editScene::EventParams ¶ms) {
|
||||
// Find the first entity with DialogueComponent
|
||||
m_world.query<DialogueComponent>().each(
|
||||
[&](flecs::entity e, DialogueComponent &dc) {
|
||||
if (!dc.enabled)
|
||||
return;
|
||||
m_showListenerId = EventBus::getInstance().subscribe(
|
||||
"dialogue_show",
|
||||
[this](const Ogre::String &,
|
||||
const editScene::EventParams ¶ms) {
|
||||
Ogre::String text = params.getString("text");
|
||||
if (text.empty())
|
||||
return;
|
||||
|
||||
Ogre::String text =
|
||||
params.getString("text");
|
||||
if (text.empty())
|
||||
return;
|
||||
std::vector<Ogre::String> choices;
|
||||
const editScene::EventValue *choicesVal =
|
||||
params.get("choices");
|
||||
if (choicesVal &&
|
||||
choicesVal->getType() ==
|
||||
editScene::EventValue::STRING_ARRAY) {
|
||||
const auto &arr = choicesVal->getStringArray();
|
||||
choices.reserve(arr.size());
|
||||
for (const auto &s : arr)
|
||||
choices.push_back(s);
|
||||
}
|
||||
|
||||
// Parse choices from string array
|
||||
std::vector<Ogre::String> choices;
|
||||
const editScene::EventValue *choicesVal =
|
||||
params.get("choices");
|
||||
if (choicesVal &&
|
||||
choicesVal->getType() ==
|
||||
editScene::EventValue::
|
||||
STRING_ARRAY) {
|
||||
const auto &arr =
|
||||
choicesVal
|
||||
->getStringArray();
|
||||
choices.reserve(arr.size());
|
||||
for (const auto &s : arr)
|
||||
choices.push_back(s);
|
||||
}
|
||||
Ogre::String speaker = params.getString("speaker");
|
||||
this->show(text, choices, speaker);
|
||||
});
|
||||
|
||||
Ogre::String speaker =
|
||||
params.getString("speaker");
|
||||
|
||||
dc.show(text, choices, speaker);
|
||||
});
|
||||
m_hideListenerId = EventBus::getInstance().subscribe(
|
||||
"dialogue_hide",
|
||||
[this](const Ogre::String &, const editScene::EventParams &) {
|
||||
this->hide();
|
||||
});
|
||||
}
|
||||
|
||||
DialogueSystem::~DialogueSystem()
|
||||
{
|
||||
// EventBus subscriptions are managed externally
|
||||
EventBus::getInstance().unsubscribe(m_showListenerId);
|
||||
EventBus::getInstance().unsubscribe(m_hideListenerId);
|
||||
}
|
||||
|
||||
DialogueSystem &DialogueSystem::getInstance()
|
||||
{
|
||||
static DialogueSystem instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void DialogueSystem::init(EditorApp *editorApp)
|
||||
{
|
||||
m_editorApp = editorApp;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool DialogueSystem::loadSettings(const std::string &path)
|
||||
{
|
||||
if (!m_settings.loadFromJson(path)) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"DialogueSystem: Could not load " + path +
|
||||
", using defaults.");
|
||||
return false;
|
||||
}
|
||||
m_fontLoaded = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DialogueSystem::saveSettings(const std::string &path) const
|
||||
{
|
||||
return m_settings.saveToJson(path);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Runtime API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void DialogueSystem::show(const Ogre::String &text,
|
||||
const std::vector<Ogre::String> &choices,
|
||||
const Ogre::String &speaker)
|
||||
{
|
||||
m_text = text;
|
||||
m_choices = choices;
|
||||
m_speaker = speaker;
|
||||
m_state = choices.empty() ? State::Showing : State::AwaitingChoice;
|
||||
if (onShow)
|
||||
onShow();
|
||||
}
|
||||
|
||||
void DialogueSystem::hide()
|
||||
{
|
||||
m_state = State::Idle;
|
||||
m_text.clear();
|
||||
m_choices.clear();
|
||||
m_speaker.clear();
|
||||
}
|
||||
|
||||
void DialogueSystem::progress()
|
||||
{
|
||||
if (m_state == State::Showing && m_choices.empty()) {
|
||||
m_state = State::Idle;
|
||||
if (onDismissed)
|
||||
onDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
void DialogueSystem::selectChoice(int index)
|
||||
{
|
||||
if (m_state == State::AwaitingChoice && index >= 1 &&
|
||||
index <= (int)m_choices.size()) {
|
||||
m_state = State::Idle;
|
||||
if (onChoiceSelected)
|
||||
onChoiceSelected(index);
|
||||
}
|
||||
}
|
||||
|
||||
bool DialogueSystem::isActive() const
|
||||
{
|
||||
return m_state != State::Idle;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Editor preview
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void DialogueSystem::setEditorPreviewEnabled(bool enabled)
|
||||
{
|
||||
m_editorPreviewEnabled = enabled;
|
||||
}
|
||||
|
||||
bool DialogueSystem::isEditorPreviewEnabled() const
|
||||
{
|
||||
return m_editorPreviewEnabled;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Font
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void DialogueSystem::ensureFontLoaded(const Ogre::String &fontName,
|
||||
float fontSize)
|
||||
float fontSize, float speakerFontSize)
|
||||
{
|
||||
if (m_fontLoaded && m_currentFontName == fontName &&
|
||||
m_currentFontSize == fontSize)
|
||||
m_currentFontSize == fontSize &&
|
||||
m_currentSpeakerFontSize == speakerFontSize)
|
||||
return;
|
||||
|
||||
if (!m_editorApp)
|
||||
return;
|
||||
|
||||
Ogre::ImGuiOverlay *overlay = m_editorApp->getImGuiOverlay();
|
||||
if (!overlay)
|
||||
return;
|
||||
|
||||
// Load the main dialogue font
|
||||
Ogre::FontPtr font;
|
||||
// Main dialogue font
|
||||
try {
|
||||
if (Ogre::FontManager::getSingleton().resourceExists(
|
||||
"DialogueFont", "General")) {
|
||||
Ogre::FontManager::getSingleton().remove("DialogueFont",
|
||||
"General");
|
||||
}
|
||||
font = Ogre::FontManager::getSingleton().create("DialogueFont",
|
||||
"General");
|
||||
Ogre::FontPtr font = Ogre::FontManager::getSingleton().create(
|
||||
"DialogueFont", "General");
|
||||
font->setType(Ogre::FontType::FT_TRUETYPE);
|
||||
font->setSource(fontName);
|
||||
font->setTrueTypeSize(fontSize);
|
||||
font->setTrueTypeResolution(75);
|
||||
font->addCodePointRange(Ogre::Font::CodePointRange(32, 255));
|
||||
font->addCodePointRange(
|
||||
Ogre::Font::CodePointRange(32, 255));
|
||||
font->load();
|
||||
m_dialogueFont = overlay->addFont("DialogueFont", "General");
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"DialogueSystem: Failed to load font " + fontName);
|
||||
m_dialogueFont = nullptr;
|
||||
m_speakerFont = nullptr;
|
||||
m_fontLoaded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_dialogueFont = overlay->addFont("DialogueFont", "General");
|
||||
// Speaker font
|
||||
try {
|
||||
if (Ogre::FontManager::getSingleton().resourceExists(
|
||||
"DialogueSpeakerFont", "General")) {
|
||||
Ogre::FontManager::getSingleton().remove(
|
||||
"DialogueSpeakerFont", "General");
|
||||
}
|
||||
Ogre::FontPtr font = Ogre::FontManager::getSingleton().create(
|
||||
"DialogueSpeakerFont", "General");
|
||||
font->setType(Ogre::FontType::FT_TRUETYPE);
|
||||
font->setSource(fontName);
|
||||
font->setTrueTypeSize(speakerFontSize);
|
||||
font->setTrueTypeResolution(75);
|
||||
font->addCodePointRange(
|
||||
Ogre::Font::CodePointRange(32, 255));
|
||||
font->load();
|
||||
m_speakerFont = overlay->addFont("DialogueSpeakerFont",
|
||||
"General");
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"DialogueSystem: Failed to load speaker font " +
|
||||
fontName);
|
||||
m_speakerFont = nullptr;
|
||||
}
|
||||
|
||||
m_currentFontName = fontName;
|
||||
m_currentFontSize = fontSize;
|
||||
m_currentSpeakerFontSize = speakerFontSize;
|
||||
m_fontLoaded = true;
|
||||
}
|
||||
|
||||
void DialogueSystem::prepareFont()
|
||||
{
|
||||
if (!m_editorApp)
|
||||
return;
|
||||
|
||||
// Find an entity with DialogueComponent
|
||||
flecs::entity dialogueEntity = flecs::entity::null();
|
||||
m_world.query<DialogueComponent>().each(
|
||||
[&](flecs::entity e, DialogueComponent &) {
|
||||
if (!dialogueEntity.is_alive())
|
||||
dialogueEntity = e;
|
||||
});
|
||||
|
||||
if (dialogueEntity.is_alive()) {
|
||||
auto &dc = dialogueEntity.get_mut<DialogueComponent>();
|
||||
ensureFontLoaded(dc.fontName, dc.fontSize);
|
||||
}
|
||||
ensureFontLoaded(m_settings.fontName, m_settings.fontSize,
|
||||
m_settings.speakerFontSize);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void DialogueSystem::update(float deltaTime)
|
||||
{
|
||||
(void)deltaTime;
|
||||
|
||||
if (!m_editorApp ||
|
||||
m_editorApp->getGameMode() != EditorApp::GameMode::Game ||
|
||||
m_editorApp->getGamePlayState() !=
|
||||
EditorApp::GamePlayState::Playing)
|
||||
bool shouldRender = false;
|
||||
if (m_editorApp) {
|
||||
bool inGame =
|
||||
m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
|
||||
m_editorApp->getGamePlayState() ==
|
||||
EditorApp::GamePlayState::Playing;
|
||||
shouldRender = inGame || m_editorPreviewEnabled;
|
||||
} else {
|
||||
shouldRender = m_editorPreviewEnabled;
|
||||
}
|
||||
|
||||
if (!shouldRender || !isActive())
|
||||
return;
|
||||
|
||||
// Find an entity with DialogueComponent
|
||||
flecs::entity dialogueEntity = flecs::entity::null();
|
||||
m_world.query<DialogueComponent>().each(
|
||||
[&](flecs::entity e, DialogueComponent &) {
|
||||
if (!dialogueEntity.is_alive())
|
||||
dialogueEntity = e;
|
||||
});
|
||||
|
||||
if (!dialogueEntity.is_alive())
|
||||
return;
|
||||
|
||||
auto &dc = dialogueEntity.get_mut<DialogueComponent>();
|
||||
if (!dc.enabled || !dc.isActive())
|
||||
return;
|
||||
|
||||
renderDialogueBox(dc);
|
||||
renderDialogueBox();
|
||||
}
|
||||
|
||||
void DialogueSystem::renderDialogueBox(DialogueComponent &dc)
|
||||
void DialogueSystem::renderDialogueBox()
|
||||
{
|
||||
ImVec2 size = ImGui::GetMainViewport()->Size;
|
||||
|
||||
float boxHeight = size.y * dc.boxHeightFraction;
|
||||
float boxY = size.y * dc.boxPositionFraction;
|
||||
float boxHeight = size.y * m_settings.boxHeightFraction;
|
||||
float boxY = size.y * m_settings.boxPositionFraction;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(0, boxY), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(size.x, boxHeight), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(size.x, boxHeight),
|
||||
ImGuiCond_Always);
|
||||
|
||||
// Semi-transparent background
|
||||
ImVec4 bgColor = ImVec4(0.0f, 0.0f, 0.0f, dc.backgroundOpacity);
|
||||
ImVec4 bgColor = ImVec4(0.0f, 0.0f, 0.0f,
|
||||
m_settings.backgroundOpacity);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, bgColor);
|
||||
|
||||
ImGui::Begin(
|
||||
"DialogueBox", nullptr,
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing);
|
||||
ImGui::Begin("DialogueBox", nullptr,
|
||||
ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing);
|
||||
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
|
||||
// Speaker name (if provided)
|
||||
if (!dc.speaker.empty()) {
|
||||
// Speaker name
|
||||
if (!m_speaker.empty()) {
|
||||
if (m_speakerFont)
|
||||
ImGui::PushFont(m_speakerFont);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "%s",
|
||||
dc.speaker.c_str());
|
||||
m_speaker.c_str());
|
||||
if (m_speakerFont)
|
||||
ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
@@ -186,7 +340,7 @@ void DialogueSystem::renderDialogueBox(DialogueComponent &dc)
|
||||
if (m_dialogueFont)
|
||||
ImGui::PushFont(m_dialogueFont);
|
||||
|
||||
ImGui::TextWrapped("%s", dc.text.c_str());
|
||||
ImGui::TextWrapped("%s", m_text.c_str());
|
||||
|
||||
if (m_dialogueFont)
|
||||
ImGui::PopFont();
|
||||
@@ -194,18 +348,16 @@ void DialogueSystem::renderDialogueBox(DialogueComponent &dc)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Choices or click-to-progress
|
||||
if (dc.choices.empty()) {
|
||||
// No choices: click anywhere to progress
|
||||
if (m_choices.empty()) {
|
||||
ImGui::SetCursorScreenPos(p);
|
||||
if (ImGui::InvisibleButton("DialogueProgress",
|
||||
ImGui::GetWindowSize())) {
|
||||
dc.progress();
|
||||
progress();
|
||||
}
|
||||
} else {
|
||||
// Choices: render as buttons
|
||||
for (int i = 0; i < (int)dc.choices.size(); i++) {
|
||||
if (ImGui::Button(dc.choices[i].c_str())) {
|
||||
dc.selectChoice(i + 1);
|
||||
for (int i = 0; i < (int)m_choices.size(); i++) {
|
||||
if (ImGui::Button(m_choices[i].c_str())) {
|
||||
selectChoice(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,57 +2,129 @@
|
||||
#define EDITSCENE_DIALOGUESYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <imgui.h>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../components/DialogueComponent.hpp"
|
||||
#include "EventBus.hpp"
|
||||
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
* System that renders the visual-novel style dialogue box in game mode.
|
||||
* Singleton dialogue system.
|
||||
*
|
||||
* Only active when EditorApp is in GameMode::Game and
|
||||
* GamePlayState::Playing. The dialogue box is rendered at the bottom
|
||||
* of the screen, showing narration text and optional player choices.
|
||||
* Manages a visual-novel style dialogue box that can be triggered
|
||||
* via direct API, Lua scripts, or EventBus events ("dialogue_show",
|
||||
* "dialogue_hide").
|
||||
*
|
||||
* The dialogue can be triggered via:
|
||||
* 1. EventBus event "dialogue_show" with EventParams payload
|
||||
* 2. Direct API on DialogueComponent
|
||||
* Visual settings are loaded from dialogue.json at startup and can be
|
||||
* tweaked from the editor or via the API.
|
||||
*/
|
||||
class DialogueSystem {
|
||||
public:
|
||||
DialogueSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
EditorApp *editorApp);
|
||||
~DialogueSystem();
|
||||
struct Settings {
|
||||
Ogre::String fontName = "Jupiteroid-Regular.ttf";
|
||||
float fontSize = 24.0f;
|
||||
float speakerFontSize = 20.0f;
|
||||
float backgroundOpacity = 0.85f;
|
||||
float boxHeightFraction = 0.25f;
|
||||
float boxPositionFraction = 0.75f;
|
||||
|
||||
/**
|
||||
* Update and render the dialogue box.
|
||||
* Must be called inside an active ImGui frame.
|
||||
*/
|
||||
bool loadFromJson(const std::string &path);
|
||||
bool saveToJson(const std::string &path) const;
|
||||
};
|
||||
|
||||
static DialogueSystem &getInstance();
|
||||
|
||||
/** Must be called before any font operations. */
|
||||
void init(EditorApp *editorApp);
|
||||
|
||||
/** Load visual settings from JSON (defaults used if file missing). */
|
||||
bool loadSettings(const std::string &path = "dialogue.json");
|
||||
/** Save visual settings to JSON. */
|
||||
bool saveSettings(const std::string &path = "dialogue.json") const;
|
||||
|
||||
const Settings &getSettings() const
|
||||
{
|
||||
return m_settings;
|
||||
}
|
||||
Settings &getSettingsRef()
|
||||
{
|
||||
return m_settings;
|
||||
}
|
||||
void setSettings(const Settings &s)
|
||||
{
|
||||
m_settings = s;
|
||||
m_fontLoaded = false;
|
||||
}
|
||||
|
||||
/* --- Runtime API --- */
|
||||
|
||||
/** Show dialogue with narration text and optional choices. */
|
||||
void show(const Ogre::String &text,
|
||||
const std::vector<Ogre::String> &choices = {},
|
||||
const Ogre::String &speaker = "");
|
||||
/** Hide the dialogue immediately. */
|
||||
void hide();
|
||||
/** Click-to-progress (no choices mode). */
|
||||
void progress();
|
||||
/** Select a choice by 1-based index. */
|
||||
void selectChoice(int index);
|
||||
/** True if dialogue is currently on screen. */
|
||||
bool isActive() const;
|
||||
|
||||
/* --- Editor preview --- */
|
||||
|
||||
void setEditorPreviewEnabled(bool enabled);
|
||||
bool isEditorPreviewEnabled() const;
|
||||
|
||||
/* --- Rendering --- */
|
||||
|
||||
/** Update and render the dialogue box. Must be inside ImGui frame. */
|
||||
void update(float deltaTime);
|
||||
|
||||
/**
|
||||
* Pre-load the dialogue font before ImGui NewFrame().
|
||||
* Must be called outside an active ImGui frame (before NewFrame).
|
||||
*/
|
||||
/** Pre-load fonts before ImGui NewFrame. */
|
||||
void prepareFont();
|
||||
|
||||
private:
|
||||
void renderDialogueBox(DialogueComponent &dc);
|
||||
void ensureFontLoaded(const Ogre::String &fontName, float fontSize);
|
||||
/* --- Callbacks --- */
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
EditorApp *m_editorApp;
|
||||
std::function<void(int)> onChoiceSelected;
|
||||
std::function<void()> onDismissed;
|
||||
std::function<void()> onShow;
|
||||
|
||||
private:
|
||||
DialogueSystem();
|
||||
~DialogueSystem();
|
||||
|
||||
DialogueSystem(const DialogueSystem &) = delete;
|
||||
DialogueSystem &operator=(const DialogueSystem &) = delete;
|
||||
|
||||
void renderDialogueBox();
|
||||
void ensureFontLoaded(const Ogre::String &fontName, float fontSize,
|
||||
float speakerFontSize);
|
||||
|
||||
EditorApp *m_editorApp = nullptr;
|
||||
|
||||
enum class State { Idle, Showing, AwaitingChoice };
|
||||
State m_state = State::Idle;
|
||||
Ogre::String m_text;
|
||||
Ogre::String m_speaker;
|
||||
std::vector<Ogre::String> m_choices;
|
||||
|
||||
Settings m_settings;
|
||||
bool m_editorPreviewEnabled = false;
|
||||
|
||||
bool m_fontLoaded = false;
|
||||
Ogre::String m_currentFontName;
|
||||
float m_currentFontSize = 0.0f;
|
||||
float m_currentSpeakerFontSize = 0.0f;
|
||||
ImFont *m_dialogueFont = nullptr;
|
||||
ImFont *m_speakerFont = nullptr;
|
||||
|
||||
EventBus::ListenerId m_showListenerId = 0;
|
||||
EventBus::ListenerId m_hideListenerId = 0;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DIALOGUESYSTEM_HPP
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "../components/GeneratedPhysicsTag.hpp"
|
||||
#include "EditorUISystem.hpp"
|
||||
#include "DialogueSystem.hpp"
|
||||
#include "PrefabSystem.hpp"
|
||||
#include "../camera/EditorCamera.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
@@ -44,6 +45,7 @@
|
||||
#include "../components/PrefabInstance.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include "../components/CharacterClassComponent.hpp"
|
||||
|
||||
#include "../ui/TransformEditor.hpp"
|
||||
#include "../ui/RenderableEditor.hpp"
|
||||
@@ -59,6 +61,8 @@
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
EditorUISystem::EditorUISystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
@@ -332,6 +336,17 @@ void EditorUISystem::update(float deltaTime)
|
||||
&m_showActionDatabaseSingleton);
|
||||
}
|
||||
|
||||
// Render Dialogue settings window
|
||||
if (m_showDialogueSettings) {
|
||||
renderDialogueSettingsWindow();
|
||||
}
|
||||
|
||||
// Render Character Class Database editor
|
||||
if (m_showCharacterClassDatabase) {
|
||||
m_characterClassDatabaseEditor.render(
|
||||
&m_showCharacterClassDatabase);
|
||||
}
|
||||
|
||||
// Render FPS overlay
|
||||
renderFPSOverlay(deltaTime);
|
||||
}
|
||||
@@ -420,6 +435,15 @@ void EditorUISystem::renderHierarchyWindow()
|
||||
"Action Database (Singleton)")) {
|
||||
m_showActionDatabaseSingleton = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Dialogue Settings")) {
|
||||
m_showDialogueSettings = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(
|
||||
"Character Class Database")) {
|
||||
m_showCharacterClassDatabase = true;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
@@ -1130,6 +1154,21 @@ 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
|
||||
|
||||
if (componentCount == 0) {
|
||||
@@ -2071,3 +2110,123 @@ void EditorUISystem::renderCursorPanel()
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
|
||||
void EditorUISystem::renderDialogueSettingsWindow()
|
||||
{
|
||||
ImGui::SetNextWindowPos(ImVec2(LEFT_PANEL_WIDTH, 100),
|
||||
ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(400, 500), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
if (!ImGui::Begin("Dialogue Settings", &m_showDialogueSettings,
|
||||
flags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
DialogueSystem &ds = DialogueSystem::getInstance();
|
||||
DialogueSystem::Settings s = ds.getSettings();
|
||||
|
||||
// --- Visual settings ---
|
||||
ImGui::Text("Visual Settings");
|
||||
ImGui::Separator();
|
||||
|
||||
char fontNameBuf[256];
|
||||
std::strncpy(fontNameBuf, s.fontName.c_str(), sizeof(fontNameBuf) - 1);
|
||||
fontNameBuf[sizeof(fontNameBuf) - 1] = '\0';
|
||||
if (ImGui::InputText("Font Name", fontNameBuf,
|
||||
sizeof(fontNameBuf))) {
|
||||
s.fontName = fontNameBuf;
|
||||
}
|
||||
|
||||
if (ImGui::DragFloat("Font Size", &s.fontSize, 0.5f, 8.0f,
|
||||
72.0f)) {
|
||||
}
|
||||
if (ImGui::DragFloat("Speaker Font Size", &s.speakerFontSize,
|
||||
0.5f, 8.0f, 72.0f)) {
|
||||
}
|
||||
if (ImGui::SliderFloat("Background Opacity", &s.backgroundOpacity,
|
||||
0.0f, 1.0f)) {
|
||||
}
|
||||
if (ImGui::SliderFloat("Box Height Fraction", &s.boxHeightFraction,
|
||||
0.05f, 0.5f)) {
|
||||
}
|
||||
if (ImGui::SliderFloat("Box Position Fraction",
|
||||
&s.boxPositionFraction, 0.0f, 1.0f)) {
|
||||
}
|
||||
|
||||
if (ImGui::Button("Apply Settings")) {
|
||||
ds.setSettings(s);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Save to dialogue.json")) {
|
||||
if (ds.saveSettings("dialogue.json")) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Dialogue settings saved.");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Load from dialogue.json")) {
|
||||
if (ds.loadSettings("dialogue.json")) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Dialogue settings loaded.");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Preview");
|
||||
ImGui::Separator();
|
||||
|
||||
static char sampleText[512] =
|
||||
"This is sample dialogue text for preview.";
|
||||
static char sampleSpeaker[128] = "Speaker";
|
||||
static char sampleChoices[512] = "Choice 1\nChoice 2\nChoice 3";
|
||||
static bool showPreview = false;
|
||||
|
||||
ImGui::InputTextMultiline("Sample Text", sampleText,
|
||||
sizeof(sampleText), ImVec2(0, 60));
|
||||
ImGui::InputText("Sample Speaker", sampleSpeaker,
|
||||
sizeof(sampleSpeaker));
|
||||
ImGui::InputTextMultiline("Sample Choices (one per line)",
|
||||
sampleChoices, sizeof(sampleChoices),
|
||||
ImVec2(0, 60));
|
||||
|
||||
if (ImGui::Checkbox("Show Preview", &showPreview)) {
|
||||
if (showPreview) {
|
||||
std::vector<Ogre::String> choices;
|
||||
std::istringstream iss(sampleChoices);
|
||||
std::string line;
|
||||
while (std::getline(iss, line)) {
|
||||
if (!line.empty())
|
||||
choices.push_back(line);
|
||||
}
|
||||
ds.show(sampleText, choices, sampleSpeaker);
|
||||
ds.setEditorPreviewEnabled(true);
|
||||
} else {
|
||||
ds.hide();
|
||||
ds.setEditorPreviewEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (showPreview && ds.isActive()) {
|
||||
if (ImGui::Button("Update Preview")) {
|
||||
std::vector<Ogre::String> choices;
|
||||
std::istringstream iss(sampleChoices);
|
||||
std::string line;
|
||||
while (std::getline(iss, line)) {
|
||||
if (!line.empty())
|
||||
choices.push_back(line);
|
||||
}
|
||||
ds.show(sampleText, choices, sampleSpeaker);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Hide Preview")) {
|
||||
showPreview = false;
|
||||
ds.hide();
|
||||
ds.setEditorPreviewEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
#include "../ui/ComponentRegistry.hpp"
|
||||
#include "../ui/ActionDatabaseSingletonEditor.hpp"
|
||||
#include "../ui/CharacterClassDatabaseEditor.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../gizmo/Gizmo.hpp"
|
||||
#include "../gizmo/Cursor3D.hpp"
|
||||
@@ -191,6 +192,7 @@ public:
|
||||
void showCreatePrefabDialog(flecs::entity entity);
|
||||
void renderPrefabBrowser();
|
||||
void renderCursorPanel();
|
||||
void renderDialogueSettingsWindow();
|
||||
|
||||
private:
|
||||
// File menu
|
||||
@@ -302,6 +304,13 @@ private:
|
||||
bool m_showActionDatabaseSingleton = false;
|
||||
ActionDatabaseSingletonEditor m_actionDatabaseSingletonEditor;
|
||||
|
||||
// Dialogue settings editor state
|
||||
bool m_showDialogueSettings = false;
|
||||
|
||||
// Character class database editor state
|
||||
bool m_showCharacterClassDatabase = false;
|
||||
CharacterClassDatabaseEditor m_characterClassDatabaseEditor;
|
||||
|
||||
// Queries
|
||||
flecs::query<EntityNameComponent> m_nameQuery;
|
||||
|
||||
|
||||
@@ -2144,10 +2144,22 @@ nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity)
|
||||
|
||||
json["age"] = cs.age;
|
||||
json["sex"] = cs.sex;
|
||||
json["outfitLevel"] = cs.outfitLevel;
|
||||
json["slots"] = nlohmann::json::object();
|
||||
for (const auto &pair : cs.slots)
|
||||
json["slots"][pair.first] = pair.second;
|
||||
|
||||
// Serialize per-layer slot selections
|
||||
nlohmann::json selections = nlohmann::json::object();
|
||||
for (const auto &pair : cs.slotSelections) {
|
||||
nlohmann::json sel;
|
||||
sel["layer1Mesh"] = pair.second.layer1Mesh;
|
||||
sel["layer2Mesh"] = pair.second.layer2Mesh;
|
||||
sel["explicitMesh"] = pair.second.explicitMesh;
|
||||
selections[pair.first] = sel;
|
||||
}
|
||||
json["slotSelections"] = selections;
|
||||
|
||||
// Serialize front axis
|
||||
json["frontAxis"] = { cs.frontAxis.x, cs.frontAxis.y, cs.frontAxis.z };
|
||||
|
||||
@@ -2155,15 +2167,37 @@ nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity)
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeCharacterSlots(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
CharacterSlotsComponent cs;
|
||||
cs.age = json.value("age", "adult");
|
||||
cs.sex = json.value("sex", "male");
|
||||
cs.outfitLevel = json.value("outfitLevel", 2);
|
||||
if (json.contains("slots") && json["slots"].is_object()) {
|
||||
for (auto &[slot, mesh] : json["slots"].items())
|
||||
cs.slots[slot] = mesh.get<std::string>();
|
||||
}
|
||||
// Deserialize per-layer slot selections
|
||||
if (json.contains("slotSelections") && json["slotSelections"].is_object()) {
|
||||
for (auto &[slot, selJson] : json["slotSelections"].items()) {
|
||||
SlotSelection sel;
|
||||
if (selJson.contains("layer1Mesh"))
|
||||
sel.layer1Mesh = selJson.value("layer1Mesh", "");
|
||||
if (selJson.contains("layer2Mesh"))
|
||||
sel.layer2Mesh = selJson.value("layer2Mesh", "");
|
||||
// Backward compat: old format had layer/requiredTags/excludedTags
|
||||
if (selJson.contains("layer") && sel.layer1Mesh.empty() &&
|
||||
sel.layer2Mesh.empty()) {
|
||||
int oldLayer = selJson.value("layer", 2);
|
||||
if (oldLayer == 1)
|
||||
sel.layer1Mesh = "auto";
|
||||
else if (oldLayer >= 2)
|
||||
sel.layer2Mesh = "auto";
|
||||
}
|
||||
sel.explicitMesh = selJson.value("explicitMesh", "");
|
||||
cs.slotSelections[slot] = sel;
|
||||
}
|
||||
}
|
||||
// Deserialize front axis
|
||||
if (json.contains("frontAxis") && json["frontAxis"].is_array() &&
|
||||
json["frontAxis"].size() >= 3) {
|
||||
|
||||
@@ -106,6 +106,7 @@ private:
|
||||
nlohmann::json serializeTriangleBuffer(flecs::entity entity);
|
||||
nlohmann::json serializeCharacter(flecs::entity entity);
|
||||
nlohmann::json serializeCharacterSlots(flecs::entity entity);
|
||||
nlohmann::json serializeCharacterShapeKeys(flecs::entity entity);
|
||||
nlohmann::json serializeAnimationTree(flecs::entity entity);
|
||||
nlohmann::json serializeAnimationTreeTemplate(flecs::entity entity);
|
||||
nlohmann::json serializeStartupMenu(flecs::entity entity);
|
||||
@@ -157,6 +158,8 @@ private:
|
||||
const nlohmann::json &json);
|
||||
void deserializeCharacterSlots(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeCharacterShapeKeys(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeAnimationTree(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeAnimationTreeTemplate(flecs::entity entity,
|
||||
@@ -211,6 +214,14 @@ private:
|
||||
void deserializeInventory(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
|
||||
// Character class serialization
|
||||
nlohmann::json serializeCharacterClass(flecs::entity entity);
|
||||
nlohmann::json serializeCharacterClassOverride(flecs::entity entity);
|
||||
void deserializeCharacterClass(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeCharacterClassOverride(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
|
||||
// AI/GOAP serialization
|
||||
nlohmann::json serializeActionDatabase();
|
||||
nlohmann::json serializeActionDebug(flecs::entity entity);
|
||||
|
||||
@@ -137,6 +137,7 @@ namespace editScene
|
||||
{
|
||||
void registerLuaComponentApi(lua_State *L);
|
||||
void registerLuaEntityApi(lua_State *L);
|
||||
void registerLuaDialogueApi(lua_State *L);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -826,26 +827,27 @@ static int testPlayerControllerComponent(lua_State *L)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 25: Dialogue component
|
||||
// Test 25: Dialogue singleton API (via LuaDialogueApi stub)
|
||||
// ---------------------------------------------------------------------------
|
||||
// DialogueSystem is no longer an ECS component. It is a singleton accessed
|
||||
// via ecs.dialogue.show(), ecs.dialogue.get_settings(), etc.
|
||||
// The full API is tested in integration tests; component_lua_test only
|
||||
// verifies that the stub table exists and is callable.
|
||||
|
||||
static int testDialogueComponent(lua_State *L)
|
||||
static int testDialogueSingletonApi(lua_State *L)
|
||||
{
|
||||
TEST("Dialogue component");
|
||||
TEST("Dialogue singleton API stub");
|
||||
|
||||
bool ok = runLua(L, "local id = ecs.create_entity();"
|
||||
"ecs.set_component(id, 'Dialogue', {"
|
||||
" text = 'Hello world',"
|
||||
" speaker = 'NPC',"
|
||||
" enabled = true"
|
||||
"});"
|
||||
"local d = ecs.get_component(id, 'Dialogue');"
|
||||
"assert(d ~= nil, 'Dialogue should exist');"
|
||||
"assert(d.text == 'Hello world', 'wrong text');"
|
||||
"assert(d.speaker == 'NPC', 'wrong speaker');"
|
||||
"assert(d.enabled == true, 'wrong enabled')");
|
||||
bool ok = runLua(L,
|
||||
"local s = ecs.dialogue.get_settings();"
|
||||
"assert(s ~= nil, 'settings should exist');"
|
||||
"assert(s.font_name ~= nil, 'font_name should exist');"
|
||||
"ecs.dialogue.show('Hello', {}, 'NPC');"
|
||||
"ecs.dialogue.hide();"
|
||||
"local active = ecs.dialogue.is_active();"
|
||||
"assert(active == false, 'should not be active after hide')");
|
||||
if (!ok)
|
||||
FAIL("Dialogue component assertion failed");
|
||||
FAIL("Dialogue singleton API assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
@@ -1833,6 +1835,7 @@ int main()
|
||||
// Register the entity and component APIs
|
||||
editScene::registerLuaEntityApi(L);
|
||||
editScene::registerLuaComponentApi(L);
|
||||
editScene::registerLuaDialogueApi(L);
|
||||
|
||||
// Run tests
|
||||
int failures = 0;
|
||||
@@ -1860,7 +1863,7 @@ int main()
|
||||
failures += testWaterPlaneComponent(L);
|
||||
failures += testBuoyancyInfoComponent(L);
|
||||
failures += testPlayerControllerComponent(L);
|
||||
failures += testDialogueComponent(L);
|
||||
failures += testDialogueSingletonApi(L);
|
||||
failures += testNavMeshComponent(L);
|
||||
failures += testSmartObjectComponent(L);
|
||||
failures += testActuatorComponent(L);
|
||||
|
||||
@@ -136,6 +136,92 @@ void registerLuaGameModeApi(lua_State *L)
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
// Stub: LuaDialogueApi (DialogueSystem singleton)
|
||||
void registerLuaDialogueApi(lua_State *L)
|
||||
{
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
}
|
||||
|
||||
lua_newtable(L);
|
||||
|
||||
// show(text, choices, speaker)
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "show");
|
||||
|
||||
// hide()
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "hide");
|
||||
|
||||
// is_active()
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
lua_pushboolean(L, 0);
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "is_active");
|
||||
|
||||
// select_choice(index)
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "select_choice");
|
||||
|
||||
// progress()
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "progress");
|
||||
|
||||
// get_settings()
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
lua_newtable(L);
|
||||
lua_pushstring(L, "Jupiteroid-Regular.ttf");
|
||||
lua_setfield(L, -2, "font_name");
|
||||
lua_pushnumber(L, 24.0);
|
||||
lua_setfield(L, -2, "font_size");
|
||||
lua_pushnumber(L, 20.0);
|
||||
lua_setfield(L, -2, "speaker_font_size");
|
||||
lua_pushnumber(L, 0.85);
|
||||
lua_setfield(L, -2, "background_opacity");
|
||||
lua_pushnumber(L, 0.25);
|
||||
lua_setfield(L, -2, "box_height_fraction");
|
||||
lua_pushnumber(L, 0.75);
|
||||
lua_setfield(L, -2, "box_position_fraction");
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "get_settings");
|
||||
|
||||
// set_settings(table)
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
return 0;
|
||||
});
|
||||
lua_setfield(L, -2, "set_settings");
|
||||
|
||||
// save_settings(path)
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "save_settings");
|
||||
|
||||
// load_settings(path)
|
||||
lua_pushcfunction(L, [](lua_State *L) -> int {
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
});
|
||||
lua_setfield(L, -2, "load_settings");
|
||||
|
||||
lua_setfield(L, -2, "dialogue");
|
||||
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
#include "CharacterClassDatabaseEditor.hpp"
|
||||
#include "../components/CharacterClassDatabase.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <cstring>
|
||||
|
||||
void CharacterClassDatabaseEditor::render(bool *open)
|
||||
{
|
||||
ImGui::SetNextWindowPos(ImVec2(300, 100), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(600, 600),
|
||||
ImGuiCond_FirstUseEver);
|
||||
|
||||
if (!ImGui::Begin("Character Class Database", open)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::Button("Save to character_class.json")) {
|
||||
if (CharacterClassDatabase::saveToJson(
|
||||
"character_class.json")) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Character class database saved.");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Load from character_class.json")) {
|
||||
if (CharacterClassDatabase::loadFromJson(
|
||||
"character_class.json")) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Character class database loaded.");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::TextDisabled("(?) Hover over fields for help.");
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Formulas support: level, current, base, value");
|
||||
ImGui::Text("Operators: + - * / ^");
|
||||
ImGui::Text("Functions: floor, ceil, min, max, clamp, sqrt, abs, round");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabBar("CCTabs")) {
|
||||
if (ImGui::BeginTabItem("Stats")) {
|
||||
renderStatsTab();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Skills")) {
|
||||
renderSkillsTab();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Needs")) {
|
||||
renderNeedsTab();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Classes")) {
|
||||
renderClassesTab();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void CharacterClassDatabaseEditor::renderStatsTab()
|
||||
{
|
||||
auto &db = CharacterClassDatabase::getSingleton();
|
||||
|
||||
ImGui::Text("Add Stat");
|
||||
ImGui::InputText("Name", m_nameBuf, sizeof(m_nameBuf));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Internal identifier, e.g. 'strength'");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::InputText("Display Name", m_displayBuf,
|
||||
sizeof(m_displayBuf));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Human-readable name shown in UI, e.g. 'Strength'");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
const char *kindLabels[] = { "Attribute", "ResourcePool" };
|
||||
ImGui::Combo("Kind", &m_statKindIdx, kindLabels,
|
||||
IM_ARRAYSIZE(kindLabels));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Attribute: permanent value (str, agi, dex)");
|
||||
ImGui::Text("ResourcePool: depletable current/max (hp, stamina, mana)");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::InputInt("Min", &m_intBuf);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Minimum value (attribute) or pool max (resource pool)");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::InputInt("Max", &m_maxVal);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Maximum value (attribute) or pool max cap (resource pool)");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
if (ImGui::Button("Add Stat")) {
|
||||
CharacterClassDatabase::StatDef def;
|
||||
def.name = m_nameBuf;
|
||||
def.displayName = m_displayBuf;
|
||||
def.kind = (m_statKindIdx == 1) ?
|
||||
CharacterClassDatabase::StatKind::ResourcePool :
|
||||
CharacterClassDatabase::StatKind::Attribute;
|
||||
def.minValue = m_intBuf;
|
||||
def.maxValue = m_maxVal;
|
||||
if (!def.name.empty())
|
||||
db.addOrReplaceStat(def);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Defined Stats");
|
||||
for (const auto &name : db.getStatNames()) {
|
||||
auto *def = db.findStat(name);
|
||||
if (!def)
|
||||
continue;
|
||||
ImGui::PushID(name.c_str());
|
||||
ImGui::Text("%s [%s] min:%d max:%d", name.c_str(),
|
||||
def->kind == CharacterClassDatabase::StatKind::ResourcePool ?
|
||||
"Pool" : "Attr",
|
||||
def->minValue, def->maxValue);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Remove"))
|
||||
db.removeStat(name);
|
||||
|
||||
if (ImGui::TreeNode("Edit")) {
|
||||
char buf[128];
|
||||
strncpy(buf, def->displayName.c_str(), sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
if (ImGui::InputText("Display Name", buf,
|
||||
sizeof(buf)))
|
||||
def->displayName = buf;
|
||||
int kindEdit = (def->kind ==
|
||||
CharacterClassDatabase::StatKind::ResourcePool) ?
|
||||
1 :
|
||||
0;
|
||||
if (ImGui::Combo("Kind", &kindEdit, kindLabels,
|
||||
IM_ARRAYSIZE(kindLabels)))
|
||||
def->kind = (kindEdit == 1) ?
|
||||
CharacterClassDatabase::StatKind::ResourcePool :
|
||||
CharacterClassDatabase::StatKind::Attribute;
|
||||
if (ImGui::InputInt("Min", &def->minValue))
|
||||
;
|
||||
if (ImGui::InputInt("Max", &def->maxValue))
|
||||
;
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterClassDatabaseEditor::renderSkillsTab()
|
||||
{
|
||||
auto &db = CharacterClassDatabase::getSingleton();
|
||||
|
||||
ImGui::Text("Add Skill");
|
||||
ImGui::InputText("Name", m_nameBuf, sizeof(m_nameBuf));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Internal identifier, e.g. 'smithing'");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::InputText("Display Name", m_displayBuf,
|
||||
sizeof(m_displayBuf));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Human-readable name shown in UI, e.g. 'Smithing'");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
if (ImGui::Button("Add Skill")) {
|
||||
CharacterClassDatabase::SkillDef def;
|
||||
def.name = m_nameBuf;
|
||||
def.displayName = m_displayBuf;
|
||||
def.maxValue = 100;
|
||||
if (!def.name.empty())
|
||||
db.addOrReplaceSkill(def);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Defined Skills");
|
||||
for (const auto &name : db.getSkillNames()) {
|
||||
auto *def = db.findSkill(name);
|
||||
if (!def)
|
||||
continue;
|
||||
ImGui::PushID(name.c_str());
|
||||
ImGui::Text("%s max:%d", name.c_str(), def->maxValue);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Remove"))
|
||||
db.removeSkill(name);
|
||||
|
||||
if (ImGui::TreeNode("Edit")) {
|
||||
char buf[128];
|
||||
strncpy(buf, def->displayName.c_str(), sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
if (ImGui::InputText("Display Name", buf,
|
||||
sizeof(buf)))
|
||||
def->displayName = buf;
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterClassDatabaseEditor::renderNeedsTab()
|
||||
{
|
||||
auto &db = CharacterClassDatabase::getSingleton();
|
||||
|
||||
ImGui::Text("Add Need");
|
||||
ImGui::InputText("Name", m_nameBuf, sizeof(m_nameBuf));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Internal identifier, e.g. 'hunger'");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::InputText("Display Name", m_displayBuf,
|
||||
sizeof(m_displayBuf));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Human-readable name shown in UI, e.g. 'Hunger'");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::InputFloat("Accumulation Rate", &m_floatBuf);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Points added per second (range 0-1000)");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::InputInt("Low Threshold", &m_lowThreshold);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Need value at or below which the bit is CLEARED");
|
||||
ImGui::Text("(need is satisfied / low priority)");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::InputInt("High Threshold", &m_highThreshold);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Need value at or above which the bit is SET");
|
||||
ImGui::Text("(need is critical / action required)");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
char bitName[128] = {};
|
||||
ImGui::InputText("Bit Name", bitName, sizeof(bitName));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("GOAP blackboard bit managed by this need");
|
||||
ImGui::Text("Set when need >= high threshold");
|
||||
ImGui::Text("Cleared when need <= low threshold");
|
||||
ImGui::Text("Between thresholds the bit keeps its current state (hysteresis)");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
if (ImGui::Button("Add Need")) {
|
||||
CharacterClassDatabase::NeedDef def;
|
||||
def.name = m_nameBuf;
|
||||
def.displayName = m_displayBuf;
|
||||
def.accumulationRate = m_floatBuf;
|
||||
def.lowThreshold = m_lowThreshold;
|
||||
def.highThreshold = m_highThreshold;
|
||||
def.bitName = bitName;
|
||||
if (!def.name.empty())
|
||||
db.addOrReplaceNeed(def);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Defined Needs");
|
||||
for (const auto &name : db.getNeedNames()) {
|
||||
auto *def = db.findNeed(name);
|
||||
if (!def)
|
||||
continue;
|
||||
ImGui::PushID(name.c_str());
|
||||
ImGui::Text("%s rate:%.1f low:%d high:%d bit:%s",
|
||||
name.c_str(), def->accumulationRate,
|
||||
def->lowThreshold, def->highThreshold,
|
||||
def->bitName.empty() ? "-" :
|
||||
def->bitName.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Remove"))
|
||||
db.removeNeed(name);
|
||||
|
||||
if (ImGui::TreeNode("Edit")) {
|
||||
char buf[128];
|
||||
strncpy(buf, def->displayName.c_str(), sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
if (ImGui::InputText("Display Name", buf,
|
||||
sizeof(buf)))
|
||||
def->displayName = buf;
|
||||
if (ImGui::InputFloat("Accumulation Rate",
|
||||
&def->accumulationRate))
|
||||
;
|
||||
if (ImGui::InputInt("Low Threshold",
|
||||
&def->lowThreshold))
|
||||
;
|
||||
if (ImGui::InputInt("High Threshold",
|
||||
&def->highThreshold))
|
||||
;
|
||||
strncpy(buf, def->bitName.c_str(), sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
if (ImGui::InputText("Bit Name", buf, sizeof(buf)))
|
||||
def->bitName = buf;
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterClassDatabaseEditor::renderClassesTab()
|
||||
{
|
||||
auto &db = CharacterClassDatabase::getSingleton();
|
||||
|
||||
ImGui::Text("Add Class");
|
||||
ImGui::InputText("Name", m_nameBuf, sizeof(m_nameBuf));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Internal identifier, e.g. 'warrior'");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::InputTextMultiline("Description", m_descBuf,
|
||||
sizeof(m_descBuf), ImVec2(0, 40));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Short description shown in UI tooltips");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::InputText("XP Formula", m_formulaBuf,
|
||||
sizeof(m_formulaBuf));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("Variables: level, current, base, value");
|
||||
ImGui::Text("Ops: + - * / ^ | Functions: floor, ceil, min, max, clamp, sqrt, abs, round");
|
||||
ImGui::Text("Example: '100 * level ^ 1.5' or 'floor(50 * sqrt(level))'");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
if (ImGui::Button("Add Class")) {
|
||||
CharacterClassDatabase::ClassDef def;
|
||||
def.name = m_nameBuf;
|
||||
def.description = m_descBuf;
|
||||
def.xpForLevel = Formula(m_formulaBuf);
|
||||
if (!def.name.empty())
|
||||
db.addOrReplaceClass(def);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Defined Classes");
|
||||
for (const auto &name : db.getClassNames()) {
|
||||
auto *cls = db.findClass(name);
|
||||
if (!cls)
|
||||
continue;
|
||||
ImGui::PushID(name.c_str());
|
||||
ImGui::Text("%s - %s", name.c_str(),
|
||||
cls->description.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Remove"))
|
||||
db.removeClass(name);
|
||||
|
||||
if (ImGui::TreeNode("Edit")) {
|
||||
char buf[256];
|
||||
strncpy(buf, cls->description.c_str(), sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
if (ImGui::InputTextMultiline("Description", buf,
|
||||
sizeof(buf),
|
||||
ImVec2(0, 40)))
|
||||
cls->description = buf;
|
||||
|
||||
char formulaBuf[256];
|
||||
strncpy(formulaBuf,
|
||||
cls->xpForLevel.getExpression().c_str(),
|
||||
sizeof(formulaBuf) - 1);
|
||||
formulaBuf[sizeof(formulaBuf) - 1] = '\0';
|
||||
if (ImGui::InputText("XP Formula", formulaBuf,
|
||||
sizeof(formulaBuf)))
|
||||
cls->xpForLevel = Formula(formulaBuf);
|
||||
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
if (ImGui::TreeNode("Edit Base Stats")) {
|
||||
for (const auto &sname : db.getStatNames()) {
|
||||
int val = cls->baseStats.count(sname) ?
|
||||
cls->baseStats.at(sname) :
|
||||
0;
|
||||
if (ImGui::InputInt(sname.c_str(), &val))
|
||||
cls->baseStats[sname] = val;
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
if (ImGui::TreeNode("Edit Base Skills")) {
|
||||
for (const auto &sname : db.getSkillNames()) {
|
||||
int val = cls->baseSkills.count(sname) ?
|
||||
cls->baseSkills.at(sname) :
|
||||
0;
|
||||
if (ImGui::InputInt(sname.c_str(), &val))
|
||||
cls->baseSkills[sname] = val;
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef EDITSCENE_CHARACTER_CLASS_DATABASE_EDITOR_HPP
|
||||
#define EDITSCENE_CHARACTER_CLASS_DATABASE_EDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <imgui.h>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Editor window for the CharacterClassDatabase singleton.
|
||||
*
|
||||
* Allows adding/removing stats, skills, needs, and classes,
|
||||
* as well as editing formulas and base values.
|
||||
*/
|
||||
class CharacterClassDatabaseEditor {
|
||||
public:
|
||||
void render(bool *open);
|
||||
|
||||
private:
|
||||
void renderStatsTab();
|
||||
void renderSkillsTab();
|
||||
void renderNeedsTab();
|
||||
void renderClassesTab();
|
||||
|
||||
// Edit buffers
|
||||
char m_nameBuf[128] = {};
|
||||
char m_displayBuf[128] = {};
|
||||
char m_formulaBuf[256] = {};
|
||||
char m_descBuf[256] = {};
|
||||
|
||||
int m_intBuf = 0;
|
||||
int m_maxVal = 999;
|
||||
float m_floatBuf = 0.0f;
|
||||
int m_lowThreshold = 0;
|
||||
int m_highThreshold = 1000;
|
||||
int m_statKindIdx = 0; // 0 = Attribute, 1 = ResourcePool
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTER_CLASS_DATABASE_EDITOR_HPP
|
||||
@@ -0,0 +1,151 @@
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#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
|
||||
@@ -0,0 +1,166 @@
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#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
|
||||
@@ -9,10 +9,9 @@ CharacterSlotsEditor::CharacterSlotsEditor(Ogre::SceneManager *sceneMgr)
|
||||
}
|
||||
|
||||
bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
|
||||
CharacterSlotsComponent &cs)
|
||||
CharacterSlotsComponent &cs)
|
||||
{
|
||||
bool modified = false;
|
||||
(void)entity;
|
||||
|
||||
if (ImGui::CollapsingHeader("Character Slots",
|
||||
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
@@ -26,8 +25,7 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
|
||||
if (ImGui::BeginCombo("Age", currentAge.c_str())) {
|
||||
for (const auto &age : ages) {
|
||||
bool isSelected = (currentAge == age);
|
||||
if (ImGui::Selectable(age.c_str(),
|
||||
isSelected)) {
|
||||
if (ImGui::Selectable(age.c_str(), isSelected)) {
|
||||
cs.age = age;
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
@@ -45,8 +43,7 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
|
||||
if (ImGui::BeginCombo("Sex", currentSex.c_str())) {
|
||||
for (const auto &sex : sexes) {
|
||||
bool isSelected = (currentSex == sex);
|
||||
if (ImGui::Selectable(sex.c_str(),
|
||||
isSelected)) {
|
||||
if (ImGui::Selectable(sex.c_str(), isSelected)) {
|
||||
cs.sex = sex;
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
@@ -59,6 +56,23 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Global outfit level */
|
||||
const char *outfitLabels[] = { "Nude", "Lingerie", "Clothed" };
|
||||
int outfit = cs.outfitLevel;
|
||||
if (outfit < 0)
|
||||
outfit = 0;
|
||||
if (outfit > 2)
|
||||
outfit = 2;
|
||||
if (ImGui::Combo("Outfit", &outfit, outfitLabels, 3)) {
|
||||
cs.outfitLevel = outfit;
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(global layer switch)");
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Front-facing axis */
|
||||
{
|
||||
Ogre::Vector3 axis = cs.frontAxis;
|
||||
@@ -75,7 +89,6 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
|
||||
cs.frontAxis.normalise();
|
||||
modified = true;
|
||||
}
|
||||
/* Quick presets */
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("-Z")) {
|
||||
cs.frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
@@ -90,54 +103,175 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Collect available and configured slots */
|
||||
/* Shape Keys */
|
||||
if (ImGui::CollapsingHeader("Shape Keys")) {
|
||||
auto shapeKeys = CharacterSlotSystem::getShapeKeyNames(
|
||||
cs.age, cs.sex);
|
||||
if (shapeKeys.empty()) {
|
||||
ImGui::TextDisabled("No shape keys available.");
|
||||
} else {
|
||||
CharacterShapeKeysComponent *skc = nullptr;
|
||||
if (entity.has<CharacterShapeKeysComponent>())
|
||||
skc = &entity.get_mut<CharacterShapeKeysComponent>();
|
||||
else {
|
||||
entity.set<CharacterShapeKeysComponent>({});
|
||||
skc = &entity.get_mut<CharacterShapeKeysComponent>();
|
||||
}
|
||||
|
||||
for (const auto &name : shapeKeys) {
|
||||
float val = skc->weights[name];
|
||||
if (ImGui::SliderFloat(name.c_str(), &val, 0.0f,
|
||||
1.0f)) {
|
||||
skc->weights[name] = val;
|
||||
skc->dirty = true;
|
||||
cs.dirty = true;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Slot selections */
|
||||
std::vector<Ogre::String> availableSlots =
|
||||
CharacterSlotSystem::getSlots(cs.age, cs.sex);
|
||||
for (const auto &pair : cs.slots) {
|
||||
if (std::find(availableSlots.begin(),
|
||||
availableSlots.end(),
|
||||
for (const auto &pair : cs.slotSelections) {
|
||||
if (std::find(availableSlots.begin(), availableSlots.end(),
|
||||
pair.first) == availableSlots.end())
|
||||
availableSlots.push_back(pair.first);
|
||||
}
|
||||
std::sort(availableSlots.begin(), availableSlots.end());
|
||||
|
||||
/* Render mesh selector for each slot */
|
||||
for (const auto &slot : availableSlots) {
|
||||
Ogre::String currentMesh = "";
|
||||
auto it = cs.slots.find(slot);
|
||||
if (it != cs.slots.end())
|
||||
currentMesh = it->second;
|
||||
/* Ensure selection exists */
|
||||
if (cs.slotSelections.find(slot) == cs.slotSelections.end()) {
|
||||
SlotSelection sel;
|
||||
/* Migrate old explicit mesh if present */
|
||||
auto oldIt = cs.slots.find(slot);
|
||||
if (oldIt != cs.slots.end())
|
||||
sel.explicitMesh = oldIt->second;
|
||||
cs.slotSelections[slot] = sel;
|
||||
}
|
||||
|
||||
std::vector<Ogre::String> meshes =
|
||||
CharacterSlotSystem::getMeshes(cs.age, cs.sex,
|
||||
slot);
|
||||
SlotSelection &sel = cs.slotSelections[slot];
|
||||
|
||||
Ogre::String label = slot;
|
||||
Ogre::String preview =
|
||||
currentMesh.empty() ? "(none)" : currentMesh;
|
||||
if (ImGui::TreeNode(slot.c_str())) {
|
||||
/* Resolved mesh preview */
|
||||
Ogre::String resolved =
|
||||
CharacterSlotSystem::resolveMesh(cs.age, cs.sex,
|
||||
slot, sel,
|
||||
cs.outfitLevel);
|
||||
ImGui::TextDisabled("Resolved: %s",
|
||||
resolved.empty() ? "(none)" :
|
||||
resolved.c_str());
|
||||
|
||||
if (ImGui::BeginCombo(label.c_str(), preview.c_str())) {
|
||||
bool noneSelected = currentMesh.empty();
|
||||
if (ImGui::Selectable("(none)", noneSelected)) {
|
||||
cs.slots[slot] = "";
|
||||
/* Explicit mesh override */
|
||||
bool useExplicit = !sel.explicitMesh.empty();
|
||||
if (ImGui::Checkbox("Lock explicit mesh", &useExplicit)) {
|
||||
if (!useExplicit) {
|
||||
sel.explicitMesh.clear();
|
||||
} else {
|
||||
/* Lock to currently resolved mesh */
|
||||
sel.explicitMesh = CharacterSlotSystem::resolveMesh(
|
||||
cs.age, cs.sex, slot, sel, cs.outfitLevel);
|
||||
}
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
if (noneSelected)
|
||||
ImGui::SetItemDefaultFocus();
|
||||
|
||||
for (const auto &mesh : meshes) {
|
||||
bool isSelected = (currentMesh == mesh);
|
||||
if (ImGui::Selectable(mesh.c_str(),
|
||||
isSelected)) {
|
||||
cs.slots[slot] = mesh;
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
if (useExplicit) {
|
||||
std::vector<Ogre::String> meshes =
|
||||
CharacterSlotSystem::getMeshes(
|
||||
cs.age, cs.sex, slot);
|
||||
Ogre::String preview =
|
||||
sel.explicitMesh.empty() ?
|
||||
"(none)" :
|
||||
sel.explicitMesh;
|
||||
if (ImGui::BeginCombo("Mesh", preview.c_str())) {
|
||||
if (ImGui::Selectable("(none)",
|
||||
sel.explicitMesh.empty())) {
|
||||
sel.explicitMesh.clear();
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
for (const auto &m : meshes) {
|
||||
bool isSelected = (sel.explicitMesh == m);
|
||||
if (ImGui::Selectable(m.c_str(), isSelected)) {
|
||||
sel.explicitMesh = m;
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
} else {
|
||||
/* Layer 1 combo */
|
||||
std::vector<Ogre::String> layer1Meshes =
|
||||
CharacterSlotSystem::getMeshesForLayer(
|
||||
cs.age, cs.sex, slot, 1);
|
||||
Ogre::String l1Preview = "none";
|
||||
if (!sel.layer1Mesh.empty())
|
||||
l1Preview = CharacterSlotSystem::getMeshLabel(
|
||||
cs.age, cs.sex, slot, sel.layer1Mesh);
|
||||
if (ImGui::BeginCombo("Lingerie (Layer 1)",
|
||||
l1Preview.c_str())) {
|
||||
if (ImGui::Selectable("none",
|
||||
sel.layer1Mesh.empty() ||
|
||||
sel.layer1Mesh == "none")) {
|
||||
sel.layer1Mesh = "none";
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
for (const auto &m : layer1Meshes) {
|
||||
Ogre::String label =
|
||||
CharacterSlotSystem::getMeshLabel(
|
||||
cs.age, cs.sex, slot, m);
|
||||
bool isSelected = (sel.layer1Mesh == m);
|
||||
if (ImGui::Selectable(label.c_str(),
|
||||
isSelected)) {
|
||||
sel.layer1Mesh = m;
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
/* Layer 2 combo */
|
||||
std::vector<Ogre::String> layer2Meshes =
|
||||
CharacterSlotSystem::getMeshesForLayer(
|
||||
cs.age, cs.sex, slot, 2);
|
||||
Ogre::String l2Preview = "none";
|
||||
if (!sel.layer2Mesh.empty())
|
||||
l2Preview = CharacterSlotSystem::getMeshLabel(
|
||||
cs.age, cs.sex, slot, sel.layer2Mesh);
|
||||
if (ImGui::BeginCombo("Clothing (Layer 2)",
|
||||
l2Preview.c_str())) {
|
||||
if (ImGui::Selectable("none",
|
||||
sel.layer2Mesh.empty() ||
|
||||
sel.layer2Mesh == "none")) {
|
||||
sel.layer2Mesh = "none";
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
for (const auto &m : layer2Meshes) {
|
||||
Ogre::String label =
|
||||
CharacterSlotSystem::getMeshLabel(
|
||||
cs.age, cs.sex, slot, m);
|
||||
bool isSelected = (sel.layer2Mesh == m);
|
||||
if (ImGui::Selectable(label.c_str(),
|
||||
isSelected)) {
|
||||
sel.layer2Mesh = m;
|
||||
modified = true;
|
||||
cs.dirty = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (isSelected)
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
#include "DialogueEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
bool DialogueEditor::renderComponent(flecs::entity entity,
|
||||
DialogueComponent &dc)
|
||||
{
|
||||
(void)entity;
|
||||
bool modified = false;
|
||||
|
||||
if (ImGui::CollapsingHeader("Dialogue Box",
|
||||
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::Indent();
|
||||
|
||||
// State display (read-only)
|
||||
const char *stateNames[] = { "Idle", "Showing",
|
||||
"AwaitingChoice" };
|
||||
ImGui::Text("State: %s", stateNames[(int)dc.state]);
|
||||
ImGui::Separator();
|
||||
|
||||
// Font configuration
|
||||
char fontNameBuf[256];
|
||||
snprintf(fontNameBuf, sizeof(fontNameBuf), "%s",
|
||||
dc.fontName.c_str());
|
||||
if (ImGui::InputText("Font Name", fontNameBuf,
|
||||
sizeof(fontNameBuf))) {
|
||||
dc.fontName = fontNameBuf;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::DragFloat("Font Size", &dc.fontSize, 0.5f, 8.0f,
|
||||
128.0f)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::DragFloat("Speaker Font Size", &dc.speakerFontSize,
|
||||
0.5f, 8.0f, 128.0f)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Visual configuration
|
||||
if (ImGui::SliderFloat("Background Opacity",
|
||||
&dc.backgroundOpacity, 0.0f, 1.0f)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::SliderFloat("Box Height Fraction",
|
||||
&dc.boxHeightFraction, 0.1f, 0.5f)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::SliderFloat("Box Position Fraction",
|
||||
&dc.boxPositionFraction, 0.0f, 1.0f)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Enabled toggle
|
||||
if (ImGui::Checkbox("Enabled", &dc.enabled))
|
||||
modified = true;
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Test buttons (only in editor mode)
|
||||
if (ImGui::Button("Test: Show Sample Text")) {
|
||||
dc.show("This is a sample narration text for testing the dialogue box layout. "
|
||||
"It should wrap properly within the box.",
|
||||
{}, "Test Speaker");
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::Button("Test: Show With Choices")) {
|
||||
std::vector<Ogre::String> testChoices = {
|
||||
"Option 1: Go left", "Option 2: Go right",
|
||||
"Option 3: Stay"
|
||||
};
|
||||
dc.show("What would you like to do?", testChoices,
|
||||
"Narrator");
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::Button("Reset Dialogue")) {
|
||||
dc.reset();
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
#ifndef EDITSCENE_DIALOGUEEDITOR_HPP
|
||||
#define EDITSCENE_DIALOGUEEDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/DialogueComponent.hpp"
|
||||
|
||||
/**
|
||||
* Editor for DialogueComponent
|
||||
*/
|
||||
class DialogueEditor : public ComponentEditor<DialogueComponent> {
|
||||
public:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
DialogueComponent &dc) override;
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Dialogue Box";
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DIALOGUEEDITOR_HPP
|
||||
Reference in New Issue
Block a user