No more animation jitter
This commit is contained in:
@@ -66,6 +66,7 @@
|
||||
#include "components/TriangleBuffer.hpp"
|
||||
#include "components/CharacterSlots.hpp"
|
||||
#include "components/CharacterIdentity.hpp"
|
||||
#include "systems/CharacterRegistry.hpp"
|
||||
#include "components/AnimationTree.hpp"
|
||||
#include "components/AnimationTreeTemplate.hpp"
|
||||
#include "components/Character.hpp"
|
||||
@@ -159,7 +160,10 @@ void ImGuiRenderListener::preViewportUpdate(
|
||||
// Render pause menu in game mode (inside ImGui frame scope)
|
||||
if (m_editorApp &&
|
||||
m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
|
||||
m_editorApp->getGamePlayState() == EditorApp::GamePlayState::Paused) {
|
||||
m_editorApp->getGamePlayState() ==
|
||||
EditorApp::GamePlayState::Paused &&
|
||||
!(m_editorApp->getCharacterClassSystem() &&
|
||||
m_editorApp->getCharacterClassSystem()->isSheetOpen())) {
|
||||
PauseMenuSystem::getInstance().update(m_deltaTime);
|
||||
}
|
||||
|
||||
@@ -408,14 +412,17 @@ void EditorApp::setup()
|
||||
m_actuatorSystem = std::make_unique<ActuatorSystem>(
|
||||
m_world, m_sceneMgr, this, m_behaviorTreeSystem.get());
|
||||
|
||||
// Setup Event Handler system
|
||||
m_eventHandlerSystem = std::make_unique<EventHandlerSystem>(
|
||||
m_world, m_behaviorTreeSystem.get());
|
||||
|
||||
// Setup Item system
|
||||
m_itemSystem = std::make_unique<ItemSystem>(
|
||||
m_world, m_sceneMgr, this, m_behaviorTreeSystem.get());
|
||||
|
||||
// Wire ItemSystem into ActuatorSystem for pickup handling
|
||||
m_actuatorSystem->setItemSystem(m_itemSystem.get());
|
||||
|
||||
// Setup Event Handler system
|
||||
m_eventHandlerSystem = std::make_unique<EventHandlerSystem>(
|
||||
m_world, m_behaviorTreeSystem.get());
|
||||
|
||||
// Wire ItemSystem into SmartObjectSystem for BT access
|
||||
m_smartObjectSystem->setItemSystem(m_itemSystem.get());
|
||||
|
||||
@@ -476,12 +483,9 @@ void EditorApp::setup()
|
||||
"container_state.json");
|
||||
|
||||
m_characterClassSystem =
|
||||
std::make_unique<CharacterClassSystem>(
|
||||
m_world, this);
|
||||
m_pregnancySystem =
|
||||
std::make_unique<PregnancySystem>(m_world);
|
||||
CharacterClassDatabase::loadFromJson(
|
||||
"character_class.json");
|
||||
std::make_unique<CharacterClassSystem>(m_world, this);
|
||||
m_pregnancySystem = std::make_unique<PregnancySystem>(m_world);
|
||||
CharacterClassDatabase::loadFromJson("character_class.json");
|
||||
|
||||
m_playerControllerSystem =
|
||||
std::make_unique<PlayerControllerSystem>(
|
||||
@@ -514,6 +518,9 @@ void EditorApp::setup()
|
||||
m_startupMenuSystem->prepareFont();
|
||||
DialogueSystem::getInstance().prepareFont();
|
||||
PauseMenuSystem::getInstance().prepareFont();
|
||||
if (m_characterClassSystem)
|
||||
m_characterClassSystem->getConfig().prepareFont(
|
||||
this);
|
||||
}
|
||||
|
||||
// Now show the overlay — font atlas will be built with our font
|
||||
@@ -690,6 +697,104 @@ void EditorApp::startNewGame(const Ogre::String &scenePath)
|
||||
setGamePlayState(GamePlayState::Playing);
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Game started: loaded scene " + scenePath);
|
||||
|
||||
// Set up player character: find the player controller entity
|
||||
// and resolve the actual target character so the character sheet
|
||||
// and inventory are on the same entity that ActuatorSystem uses.
|
||||
flecs::entity playerController;
|
||||
m_world.query<EntityNameComponent>().each(
|
||||
[&](flecs::entity e, EntityNameComponent &name) {
|
||||
if (name.name == "player" && e.is_alive()) {
|
||||
playerController = e;
|
||||
}
|
||||
});
|
||||
flecs::entity playerCharacter = playerController;
|
||||
if (playerController.is_alive() &&
|
||||
playerController.has<PlayerControllerComponent>()) {
|
||||
auto &pc = playerController.get<PlayerControllerComponent>();
|
||||
if (!pc.targetCharacterName.empty()) {
|
||||
m_world.query<EntityNameComponent>().each(
|
||||
[&](flecs::entity e,
|
||||
EntityNameComponent &en) {
|
||||
if (en.name == pc.targetCharacterName)
|
||||
playerCharacter = e;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (playerCharacter.is_valid() && playerCharacter.is_alive()) {
|
||||
// Check if player already has a character identity
|
||||
if (!playerCharacter.has<CharacterIdentityComponent>()) {
|
||||
// Create a character record for the player
|
||||
uint64_t charId =
|
||||
CharacterRegistry::getSingleton()
|
||||
.createCharacter("Player",
|
||||
"One", "",
|
||||
false);
|
||||
// Link the entity to the registry record
|
||||
playerCharacter.set<CharacterIdentityComponent>(
|
||||
CharacterIdentityComponent{ charId });
|
||||
|
||||
// Set default class and initial stats from
|
||||
// the database
|
||||
auto *rec = CharacterRegistry::getSingleton()
|
||||
.findCharacter(charId);
|
||||
if (rec) {
|
||||
auto &db = CharacterClassDatabase::
|
||||
getSingleton();
|
||||
// Use "pclass" for the player if it
|
||||
// exists, otherwise fall back to the
|
||||
// first available class
|
||||
const auto &classNames =
|
||||
db.getClassNames();
|
||||
rec->className = "pclass";
|
||||
bool found = false;
|
||||
for (const auto &cn : classNames) {
|
||||
if (cn == "pclass") {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found && !classNames.empty()) {
|
||||
rec->className = classNames[0];
|
||||
}
|
||||
const auto *cls =
|
||||
db.findClass(rec->className);
|
||||
if (cls) {
|
||||
// Initialize stats from
|
||||
// baseStats
|
||||
for (const auto &pair :
|
||||
cls->baseStats) {
|
||||
rec->stats[pair.first] =
|
||||
pair.second;
|
||||
}
|
||||
// Initialize needs
|
||||
auto needNames =
|
||||
db.getNeedNames();
|
||||
for (const auto &n :
|
||||
needNames) {
|
||||
const auto *def =
|
||||
db.findNeed(n);
|
||||
if (def)
|
||||
rec->needs[n] =
|
||||
0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"EditorApp: created character record for player entity " +
|
||||
Ogre::StringConverter::toString(
|
||||
(int)playerCharacter.id()));
|
||||
}
|
||||
// Ensure player has an inventory for the character sheet
|
||||
if (!playerCharacter.has<InventoryComponent>()) {
|
||||
playerCharacter.set<InventoryComponent>(
|
||||
InventoryComponent());
|
||||
}
|
||||
} else {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"EditorApp: no player entity found in scene");
|
||||
}
|
||||
|
||||
// Send "scene_ready" event after scene is loaded and
|
||||
// entities/components are populated and ready to run.
|
||||
EventBus::getInstance().send("scene_ready");
|
||||
@@ -757,7 +862,6 @@ void EditorApp::setupECS()
|
||||
m_world.component<PlayerControllerComponent>();
|
||||
m_world.component<InWater>();
|
||||
|
||||
|
||||
// Register environment components
|
||||
m_world.component<SunComponent>();
|
||||
m_world.component<SkyboxComponent>();
|
||||
@@ -939,18 +1043,18 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Visual mesh setup (must run before animation) --- */
|
||||
if (m_characterSlotSystem) {
|
||||
m_characterSlotSystem->update();
|
||||
}
|
||||
|
||||
/* --- Gameplay systems (paused when in Paused state) --- */
|
||||
if (!paused) {
|
||||
/* --- Visual mesh setup (must run before animation) --- */
|
||||
if (m_characterSlotSystem) {
|
||||
m_characterSlotSystem->update();
|
||||
}
|
||||
/* --- Animation / procedural generation --- */
|
||||
if (m_animationTreeSystem) {
|
||||
m_animationTreeSystem->update(evt.timeSinceLastFrame);
|
||||
if (m_behaviorTreeSystem)
|
||||
m_behaviorTreeSystem->update(evt.timeSinceLastFrame);
|
||||
m_behaviorTreeSystem->update(
|
||||
evt.timeSinceLastFrame);
|
||||
}
|
||||
if (m_pathFollowingSystem) {
|
||||
m_pathFollowingSystem->update(evt.timeSinceLastFrame);
|
||||
@@ -1011,7 +1115,8 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
if (m_buoyancySystem) {
|
||||
// Update camera position for water detection area
|
||||
if (m_camera) {
|
||||
Ogre::Vector3 cameraPos = m_camera->getPosition();
|
||||
Ogre::Vector3 cameraPos =
|
||||
m_camera->getPosition();
|
||||
m_buoyancySystem->setCameraPosition(cameraPos);
|
||||
}
|
||||
m_buoyancySystem->update(evt.timeSinceLastFrame);
|
||||
@@ -1062,6 +1167,68 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
m_proceduralMaterialSystem->update();
|
||||
}
|
||||
|
||||
// Handle "I" key to toggle character sheet (iPressed is valid here,
|
||||
// set by keyPressed which fires between frames via messagePump)
|
||||
if (m_gameInput.iPressed) {
|
||||
Ogre::String modeStr =
|
||||
(m_gameMode == GameMode::Game) ? "Game" : "Editor";
|
||||
Ogre::String stateStr =
|
||||
(m_gamePlayState == GamePlayState::Playing) ?
|
||||
"Playing" :
|
||||
(m_gamePlayState == GamePlayState::Paused) ? "Paused" :
|
||||
"Menu";
|
||||
Ogre::String ccsStr = m_characterClassSystem ? "valid" : "null";
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"EditorApp: iPressed detected, mode=" + modeStr +
|
||||
" state=" + stateStr + " ccs=" + ccsStr);
|
||||
}
|
||||
if (m_gameMode == GameMode::Game && m_gameInput.iPressed &&
|
||||
m_characterClassSystem) {
|
||||
if (m_characterClassSystem->isSheetOpen()) {
|
||||
// Close sheet and unpause
|
||||
m_characterClassSystem->toggleCharacterSheet(
|
||||
flecs::entity::null());
|
||||
setGamePlayState(GamePlayState::Playing);
|
||||
} else if (m_gamePlayState == GamePlayState::Playing) {
|
||||
// Find player controller and resolve target character
|
||||
// so the sheet shows the same entity ActuatorSystem uses.
|
||||
flecs::entity playerController;
|
||||
m_world.query<EntityNameComponent>().each(
|
||||
[&](flecs::entity e,
|
||||
EntityNameComponent &name) {
|
||||
if (name.name == "player" &&
|
||||
e.is_alive()) {
|
||||
playerController = e;
|
||||
}
|
||||
});
|
||||
flecs::entity playerCharacter = playerController;
|
||||
if (playerController.is_alive() &&
|
||||
playerController.has<PlayerControllerComponent>()) {
|
||||
auto &pc = playerController.get<PlayerControllerComponent>();
|
||||
if (!pc.targetCharacterName.empty()) {
|
||||
m_world.query<EntityNameComponent>().each(
|
||||
[&](flecs::entity e,
|
||||
EntityNameComponent &en) {
|
||||
if (en.name == pc.targetCharacterName)
|
||||
playerCharacter = e;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (playerCharacter.is_valid() && playerCharacter.is_alive()) {
|
||||
m_characterClassSystem->toggleCharacterSheet(
|
||||
playerCharacter);
|
||||
setGamePlayState(GamePlayState::Paused);
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"EditorApp: opened sheet for player entity " +
|
||||
Ogre::StringConverter::toString(
|
||||
(int)playerCharacter.id()));
|
||||
} else {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"EditorApp: no player entity found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset per-frame input state
|
||||
m_gameInput.resetPerFrame();
|
||||
|
||||
@@ -1192,6 +1359,11 @@ bool EditorApp::keyPressed(const OgreBites::KeyboardEvent &evt)
|
||||
m_gameInput.f = pressed;
|
||||
m_gameInput.fPressed = true;
|
||||
break;
|
||||
case 'i':
|
||||
case 'I':
|
||||
m_gameInput.i = pressed;
|
||||
m_gameInput.iPressed = true;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1256,6 +1428,10 @@ bool EditorApp::keyReleased(const OgreBites::KeyboardEvent &evt)
|
||||
case 'F':
|
||||
m_gameInput.f = pressed;
|
||||
break;
|
||||
case 'i':
|
||||
case 'I':
|
||||
m_gameInput.i = pressed;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1302,19 +1478,21 @@ void EditorApp::locateResources()
|
||||
const char *female;
|
||||
};
|
||||
CharPathPair charPaths[] = {
|
||||
{"./characters/male", "./characters/female"},
|
||||
{"../../../characters/male", "../../../characters/female"},
|
||||
{"../../characters/male", "../../characters/female"},
|
||||
{"../characters/male", "../characters/female"},
|
||||
{ "./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::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));
|
||||
|
||||
@@ -60,8 +60,10 @@ struct GameInputState {
|
||||
bool shift = false;
|
||||
bool e = false;
|
||||
bool f = false;
|
||||
bool i = false;
|
||||
bool ePressed = false;
|
||||
bool fPressed = false;
|
||||
bool iPressed = false;
|
||||
float mouseDeltaX = 0.0f;
|
||||
float mouseDeltaY = 0.0f;
|
||||
bool mouseMoved = false;
|
||||
@@ -73,6 +75,7 @@ struct GameInputState {
|
||||
mouseDeltaY = 0.0f;
|
||||
ePressed = false;
|
||||
fPressed = false;
|
||||
iPressed = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -772,6 +772,15 @@ public:
|
||||
&temp_allocator, &job_system);
|
||||
timeAccumulator -= fixedDeltaTime;
|
||||
}
|
||||
/* Always consume remaining time with a partial step so
|
||||
* physics never sits idle for a frame. This keeps character
|
||||
* movement in sync with animation root motion which advances
|
||||
* every frame regardless of physics step timing. */
|
||||
if (timeAccumulator > 0.0001f) {
|
||||
physics_system.Update(timeAccumulator, cCollisionSteps,
|
||||
&temp_allocator, &job_system);
|
||||
timeAccumulator = 0;
|
||||
}
|
||||
for (JPH::BodyID bID : bodies) {
|
||||
JPH::RVec3 p;
|
||||
JPH::Quat q;
|
||||
@@ -784,11 +793,15 @@ public:
|
||||
if (body_interface.GetMotionType(bID) !=
|
||||
JPH::EMotionType::Dynamic)
|
||||
continue;
|
||||
/* Skip character bodies - they are handled separately */
|
||||
if (characterBodies.find(bID) != characterBodies.end())
|
||||
continue;
|
||||
body_interface.GetPositionAndRotation(bID, p, q);
|
||||
Ogre::SceneNode *node = id2node[bID];
|
||||
node->_setDerivedPosition(JoltPhysics::convert(p));
|
||||
node->_setDerivedOrientation(JoltPhysics::convert(q));
|
||||
}
|
||||
/* Sync character positions back to scene nodes */
|
||||
for (JPH::Character *ch : characters) {
|
||||
JPH::BodyID bID = ch->GetBodyID();
|
||||
if (body_interface.IsAdded(bID)) {
|
||||
@@ -800,7 +813,6 @@ public:
|
||||
ch->GetPosition()));
|
||||
}
|
||||
}
|
||||
|
||||
if (debugDraw)
|
||||
physics_system.DrawBodies(
|
||||
JPH::BodyManager::DrawSettings(),
|
||||
|
||||
@@ -48,20 +48,32 @@ Ogre::Vector2 ActuatorSystem::projectToScreen(const Ogre::Vector3 &worldPoint)
|
||||
float width = vpSize.x;
|
||||
float height = vpSize.y;
|
||||
|
||||
// Convert to camera space
|
||||
// 1. Convert to camera space (OGRE camera looks down -Z)
|
||||
Ogre::Vector3 eyeSpacePoint = camera->getViewMatrix() * worldPoint;
|
||||
|
||||
// Project to clip space
|
||||
Ogre::Vector3 clipSpacePoint =
|
||||
camera->getProjectionMatrix() * eyeSpacePoint;
|
||||
if (clipSpacePoint.z < 0.0f)
|
||||
if (eyeSpacePoint.z >= 0.0f)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
// Convert from clip space (-1 to 1) to screen space (0 to 1)
|
||||
float screenX = (clipSpacePoint.x / 2.0f) + 0.5f;
|
||||
float screenY = 1.0f - ((clipSpacePoint.y / 2.0f) + 0.5f);
|
||||
// 2. Project to homogeneous clip space using Vector4 to preserve W
|
||||
Ogre::Vector4 clipSpacePoint = camera->getProjectionMatrix() *
|
||||
Ogre::Vector4(eyeSpacePoint.x, eyeSpacePoint.y,
|
||||
eyeSpacePoint.z, 1.0f);
|
||||
if (clipSpacePoint.w <= 0.0f)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
// Map to actual pixel dimensions
|
||||
// 3. Perspective divide to get NDC [-1, 1]
|
||||
float ndcX = clipSpacePoint.x / clipSpacePoint.w;
|
||||
float ndcY = clipSpacePoint.y / clipSpacePoint.w;
|
||||
|
||||
// 4. Convert NDC to screen space [0, 1], flipping Y for ImGui
|
||||
float screenX = (ndcX * 0.5f) + 0.5f;
|
||||
float screenY = 1.0f - ((ndcY * 0.5f) + 0.5f);
|
||||
|
||||
// 5. Reject if outside viewport bounds
|
||||
if (screenX < 0.0f || screenX > 1.0f ||
|
||||
screenY < 0.0f || screenY > 1.0f)
|
||||
return Ogre::Vector2(-1, -1);
|
||||
|
||||
// 6. Map to actual pixel dimensions
|
||||
return Ogre::Vector2(screenX * width, screenY * height);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,19 @@
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
|
||||
static unsigned short findBoneBlendIndex(Ogre::SkeletonInstance *skel, Ogre::Bone *bone)
|
||||
{
|
||||
unsigned short handle = bone->getHandle();
|
||||
if (handle < skel->getNumBones() && skel->getBone(handle) == bone)
|
||||
return handle;
|
||||
/* Fallback: search by pointer in case handles are not sequential */
|
||||
for (unsigned short i = 0; i < skel->getNumBones(); ++i) {
|
||||
if (skel->getBone(i) == bone)
|
||||
return i;
|
||||
}
|
||||
return (unsigned short)-1;
|
||||
}
|
||||
|
||||
AnimationTreeSystem::AnimationTreeSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr)
|
||||
: m_world(world)
|
||||
@@ -87,6 +100,17 @@ bool AnimationTreeSystem::setupEntity(flecs::entity e,
|
||||
}
|
||||
|
||||
EntityAnimTreeState &state = m_states[e.id()];
|
||||
/* Preserve root-motion tracking state across rebuilds so
|
||||
* setupEntity() does not produce a one-frame zero-delta
|
||||
* stutter when the mesh is recreated by CharacterSlotSystem.
|
||||
*/
|
||||
std::unordered_map<Ogre::String, std::pair<Ogre::Vector3, bool>>
|
||||
prevRootMotion;
|
||||
for (const auto &pair : state.animations) {
|
||||
prevRootMotion[pair.first] =
|
||||
{ pair.second.prevRootPos,
|
||||
pair.second.hasPrevRootPos };
|
||||
}
|
||||
state.ogreEntity = ent;
|
||||
state.ogreEntityName = ent->getName();
|
||||
state.rootBone = nullptr;
|
||||
@@ -128,8 +152,15 @@ bool AnimationTreeSystem::setupEntity(flecs::entity e,
|
||||
if (state.rootBone) {
|
||||
as->destroyBlendMask();
|
||||
as->createBlendMask(skel->getNumBones(), 1.0f);
|
||||
as->setBlendMaskEntry(
|
||||
state.rootBone->getHandle(), 0.0f);
|
||||
unsigned short blendIdx = findBoneBlendIndex(skel, state.rootBone);
|
||||
if (blendIdx != (unsigned short)-1) {
|
||||
as->setBlendMaskEntry(blendIdx, 0.0f);
|
||||
} else {
|
||||
std::cout << "AnimationTreeSystem::setupEntity: WARNING could not find blend mask index for root bone "
|
||||
<< state.rootBone->getName()
|
||||
<< " handle=" << state.rootBone->getHandle()
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
AnimationRuntimeInfo info;
|
||||
@@ -148,18 +179,28 @@ bool AnimationTreeSystem::setupEntity(flecs::entity e,
|
||||
if (info.rootTrack
|
||||
->getNumKeyFrames() >=
|
||||
2) {
|
||||
Ogre::TransformKeyFrame *tkfBeg =
|
||||
info.rootTrack
|
||||
->getNodeKeyFrame(
|
||||
0);
|
||||
Ogre::TransformKeyFrame *tkfEnd =
|
||||
info.rootTrack->getNodeKeyFrame(
|
||||
info.rootTrack
|
||||
->getNumKeyFrames() -
|
||||
1);
|
||||
/* Use interpolated keyframes at the exact
|
||||
* time boundaries (0 and length) rather
|
||||
* than the first/last stored keyframes.
|
||||
* Exporters sometimes place keyframes
|
||||
* slightly inside the boundaries, which
|
||||
* would make loopTranslation slightly
|
||||
* wrong and cause a backward lurch on
|
||||
* wrap frames. */
|
||||
Ogre::TransformKeyFrame tkfBeg(
|
||||
info.rootTrack, 0.0f);
|
||||
Ogre::TransformKeyFrame tkfEnd(
|
||||
info.rootTrack, 0.0f);
|
||||
info.rootTrack
|
||||
->getInterpolatedKeyFrame(
|
||||
0.0f, &tkfBeg);
|
||||
info.rootTrack
|
||||
->getInterpolatedKeyFrame(
|
||||
as->getLength(),
|
||||
&tkfEnd);
|
||||
info.loopTranslation =
|
||||
tkfEnd->getTranslate() -
|
||||
tkfBeg->getTranslate();
|
||||
tkfEnd.getTranslate() -
|
||||
tkfBeg.getTranslate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +211,17 @@ bool AnimationTreeSystem::setupEntity(flecs::entity e,
|
||||
}
|
||||
}
|
||||
|
||||
/* Restore root-motion tracking state for animations that
|
||||
* existed before the rebuild.
|
||||
*/
|
||||
for (auto &pair : state.animations) {
|
||||
auto it = prevRootMotion.find(pair.first);
|
||||
if (it != prevRootMotion.end()) {
|
||||
pair.second.prevRootPos = it->second.first;
|
||||
pair.second.hasPrevRootPos = it->second.second;
|
||||
}
|
||||
}
|
||||
|
||||
/* Initialize default state machine states */
|
||||
initializeTreeStates(at.root, at);
|
||||
std::cout
|
||||
@@ -334,6 +386,19 @@ void AnimationTreeSystem::update(float deltaTime)
|
||||
if (!state.ogreEntity)
|
||||
return;
|
||||
|
||||
/* Diagnostic: check if OGRE moved the root bone during rendering */
|
||||
if (state.rootBone) {
|
||||
Ogre::Vector3 currentPos = state.rootBone->getPosition();
|
||||
Ogre::Vector3 diff = currentPos - state.rootBindingPosition;
|
||||
if (diff.squaredLength() > 0.0001f) {
|
||||
std::cout << "[ANIM_DBG] ROOT BONE MOVED! entity=" << e.id()
|
||||
<< " current=" << currentPos
|
||||
<< " binding=" << state.rootBindingPosition
|
||||
<< " diff=" << diff
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
if (!at.enabled) {
|
||||
disableAllAnimations(state);
|
||||
return;
|
||||
@@ -378,77 +443,101 @@ void AnimationTreeSystem::update(float deltaTime)
|
||||
|
||||
if (at.useRootMotion && info.rootTrack) {
|
||||
Ogre::TransformKeyFrame tkf(nullptr,
|
||||
0.0f);
|
||||
0.0f);
|
||||
info.rootTrack->getInterpolatedKeyFrame(
|
||||
thisTime, &tkf);
|
||||
Ogre::Vector3 thisPos =
|
||||
tkf.getTranslate();
|
||||
|
||||
Ogre::Vector3 delta;
|
||||
if (info.hasPrevRootPos) {
|
||||
/*
|
||||
* Compute delta from previous
|
||||
* root bone position. Detect
|
||||
* animation wrapping by checking
|
||||
* if time decreased (wrapped
|
||||
* around). When wrapping, add
|
||||
* the loop translation to
|
||||
* compensate for the jump from
|
||||
* end back to start.
|
||||
* For looping animations, use the average
|
||||
* cycle velocity (loopTranslation / duration)
|
||||
* instead of exact frame-to-frame displacement.
|
||||
* The raw root bone oscillates back and forth
|
||||
* within a walk cycle; transferring that
|
||||
* oscillation to world position makes the
|
||||
* whole character jitter. The average velocity
|
||||
* captures only the net forward movement.
|
||||
* Non-looping animations still use exact
|
||||
* displacement so acceleration curves are
|
||||
* preserved.
|
||||
*/
|
||||
delta = thisPos -
|
||||
info.prevRootPos;
|
||||
if (thisTime < lastTime) {
|
||||
/* Animation wrapped */
|
||||
delta +=
|
||||
info.loopTranslation;
|
||||
float animLen = info.ogreAnimState->getLength();
|
||||
if (
|
||||
animLen > 0.0001f &&
|
||||
info.loopTranslation.squaredLength() > 0.0001f) {
|
||||
Ogre::Vector3 avgVelLocal =
|
||||
info.loopTranslation / animLen;
|
||||
totalRootMotion +=
|
||||
avgVelLocal *
|
||||
data.timeDelta *
|
||||
data.weight;
|
||||
} else if (info.hasPrevRootPos) {
|
||||
Ogre::Vector3 displacement =
|
||||
thisPos -
|
||||
info.prevRootPos;
|
||||
if (thisTime < lastTime)
|
||||
displacement +=
|
||||
info.loopTranslation;
|
||||
totalRootMotion +=
|
||||
displacement *
|
||||
data.weight;
|
||||
} else {
|
||||
/* Fallback for first frame:
|
||||
* instantaneous velocity */
|
||||
float sampleDt = 0.001f;
|
||||
float nextTime = thisTime + sampleDt;
|
||||
bool wrapped = false;
|
||||
if (nextTime > info.ogreAnimState->getLength()) {
|
||||
nextTime -= info.ogreAnimState->getLength();
|
||||
wrapped = true;
|
||||
}
|
||||
Ogre::TransformKeyFrame tkfNext(nullptr, 0.0f);
|
||||
info.rootTrack->getInterpolatedKeyFrame(
|
||||
nextTime, &tkfNext);
|
||||
Ogre::Vector3 nextPos = tkfNext.getTranslate();
|
||||
if (wrapped)
|
||||
nextPos += info.loopTranslation;
|
||||
Ogre::Vector3 velocityLocal = (nextPos - thisPos) / sampleDt;
|
||||
totalRootMotion += velocityLocal * data.timeDelta * data.weight;
|
||||
}
|
||||
} else {
|
||||
delta = Ogre::Vector3::ZERO;
|
||||
}
|
||||
|
||||
info.prevRootPos = thisPos;
|
||||
info.hasPrevRootPos = true;
|
||||
|
||||
totalRootMotion += delta * data.weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (at.useRootMotion && sceneNode) {
|
||||
/*
|
||||
* Compute root motion velocity from the animation
|
||||
* displacement. Do NOT move the scene node directly -
|
||||
* the physics character's velocity drives movement,
|
||||
* and physics writes the position back to the scene
|
||||
* node naturally. This avoids jitter caused by
|
||||
* teleporting the physics character to match a
|
||||
* root-motion-driven scene node position.
|
||||
*/
|
||||
if (e.has<CharacterComponent>()) {
|
||||
auto &cc = e.get_mut<CharacterComponent>();
|
||||
cc.useRootMotion = true;
|
||||
if (deltaTime > 0.0000001f) {
|
||||
float safeDelta = Ogre::Math::Clamp(
|
||||
deltaTime, 0.005f, 0.99f);
|
||||
Ogre::Quaternion worldRot =
|
||||
sceneNode
|
||||
->_getDerivedOrientation();
|
||||
cc.linearVelocity = worldRot *
|
||||
totalRootMotion /
|
||||
safeDelta;
|
||||
cc.linearVelocity.x = Ogre::Math::Clamp(
|
||||
cc.linearVelocity.x, -16.0f,
|
||||
16.0f);
|
||||
cc.linearVelocity.z = Ogre::Math::Clamp(
|
||||
cc.linearVelocity.z, -16.0f,
|
||||
16.0f);
|
||||
cc.linearVelocity.y = Ogre::Math::Clamp(
|
||||
cc.linearVelocity.y, -10.5f,
|
||||
10.0f);
|
||||
if (at.useRootMotion && sceneNode) {
|
||||
/* Smooth root-motion displacement with EMA to
|
||||
* filter high-frequency hip oscillation within
|
||||
* the walk cycle. Alpha = 0.15 gives strong
|
||||
* attenuation of ~2 Hz walk-cycle jitter while
|
||||
* still responding to stops within ~6 frames. */
|
||||
float alpha = 0.15f;
|
||||
if (state.hasSmoothedRootMotion) {
|
||||
totalRootMotion =
|
||||
state.smoothedRootMotion *
|
||||
(1.0f - alpha) +
|
||||
totalRootMotion * alpha;
|
||||
}
|
||||
state.smoothedRootMotion = totalRootMotion;
|
||||
state.hasSmoothedRootMotion = true;
|
||||
|
||||
Ogre::Quaternion worldRot =
|
||||
sceneNode->_getDerivedOrientation();
|
||||
Ogre::Vector3 displacementWorld =
|
||||
worldRot * totalRootMotion;
|
||||
|
||||
if (e.has<CharacterComponent>()) {
|
||||
auto &cc = e.get_mut<CharacterComponent>();
|
||||
cc.useRootMotion = true;
|
||||
if (deltaTime > 0.0000001f) {
|
||||
cc.linearVelocity =
|
||||
displacementWorld / deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Reset root bone to binding pose */
|
||||
if (state.rootBone) {
|
||||
state.rootBone->setPosition(state.rootBindingPosition);
|
||||
|
||||
@@ -77,6 +77,12 @@ private:
|
||||
Ogre::Quaternion appliedRootRotation =
|
||||
Ogre::Quaternion::IDENTITY;
|
||||
|
||||
/* Previous root-motion velocity for smoothing */
|
||||
Ogre::Vector3 prevRootMotionVel = Ogre::Vector3::ZERO;
|
||||
/* Smoothed root-motion displacement (EMA) */
|
||||
Ogre::Vector3 smoothedRootMotion = Ogre::Vector3::ZERO;
|
||||
bool hasSmoothedRootMotion = false;
|
||||
|
||||
std::unordered_map<Ogre::String, AnimationRuntimeInfo>
|
||||
animations;
|
||||
std::unordered_map<Ogre::String, FadeInfo> fadeStates;
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
#include "CharacterClassSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "CharacterRegistry.hpp"
|
||||
#include "ItemRegistry.hpp"
|
||||
#include "../components/CharacterClassDatabase.hpp"
|
||||
#include "../components/CharacterIdentity.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/PlayerController.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreFontManager.h>
|
||||
#include <OgreImGuiOverlay.h>
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Helpers
|
||||
@@ -78,10 +82,17 @@ CharacterClassSystem::CharacterClassSystem(flecs::world &world,
|
||||
: m_world(world)
|
||||
, m_editorApp(editorApp)
|
||||
{
|
||||
// Load inventory dialog configuration
|
||||
m_config.loadFromFile("inventory_config.json");
|
||||
}
|
||||
|
||||
CharacterClassSystem::~CharacterClassSystem()
|
||||
{
|
||||
// Save inventory dialog configuration only in editor mode
|
||||
if (m_editorApp &&
|
||||
m_editorApp->getGameMode() == EditorApp::GameMode::Editor) {
|
||||
m_config.saveToFile("inventory_config.json");
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
@@ -101,8 +112,9 @@ bool CharacterClassSystem::addXP(flecs::entity entity, int64_t amount)
|
||||
if (!cls)
|
||||
return false;
|
||||
|
||||
int64_t needed = CharacterClassDatabase::getSingleton()
|
||||
.computeXPForLevel(rec->level, *cls);
|
||||
int64_t needed =
|
||||
CharacterClassDatabase::getSingleton().computeXPForLevel(
|
||||
rec->level, *cls);
|
||||
if (rec->currentXP >= needed) {
|
||||
applyLevelUp(entity);
|
||||
return true;
|
||||
@@ -125,8 +137,8 @@ bool CharacterClassSystem::distributePoint(flecs::entity entity,
|
||||
if (!cls)
|
||||
return false;
|
||||
|
||||
const auto *statDef = CharacterClassDatabase::getSingleton().findStat(
|
||||
statName);
|
||||
const auto *statDef =
|
||||
CharacterClassDatabase::getSingleton().findStat(statName);
|
||||
if (!statDef)
|
||||
return false;
|
||||
|
||||
@@ -147,10 +159,28 @@ bool CharacterClassSystem::distributePoint(flecs::entity entity,
|
||||
* Update
|
||||
* --------------------------------------------------------------------------- */
|
||||
|
||||
void CharacterClassSystem::toggleCharacterSheet(flecs::entity entity)
|
||||
{
|
||||
if (!entity.is_alive()) {
|
||||
// Null or dead entity: close all sheets
|
||||
m_sheets.clear();
|
||||
return;
|
||||
}
|
||||
flecs::entity_t id = entity.id();
|
||||
if (m_sheets.count(id)) {
|
||||
m_sheets.erase(id);
|
||||
} else {
|
||||
m_sheets.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterClassSystem::update(float deltaTime)
|
||||
{
|
||||
accumulateNeeds(deltaTime);
|
||||
checkLevelUps();
|
||||
|
||||
// "I" key handling is now done in EditorApp::frameRenderingQueued
|
||||
// where iPressed is valid (set by keyPressed between frames).
|
||||
}
|
||||
|
||||
void CharacterClassSystem::accumulateNeeds(float deltaTime)
|
||||
@@ -159,8 +189,9 @@ void CharacterClassSystem::accumulateNeeds(float deltaTime)
|
||||
|
||||
m_world.query<CharacterIdentityComponent>().each(
|
||||
[&](flecs::entity e, CharacterIdentityComponent &ci) {
|
||||
auto *rec = CharacterRegistry::getSingleton().findCharacter(
|
||||
ci.registryId);
|
||||
auto *rec =
|
||||
CharacterRegistry::getSingleton().findCharacter(
|
||||
ci.registryId);
|
||||
if (!rec || rec->className.empty())
|
||||
return;
|
||||
const auto *cls = db.findClass(rec->className);
|
||||
@@ -172,8 +203,8 @@ void CharacterClassSystem::accumulateNeeds(float deltaTime)
|
||||
if (!needDef)
|
||||
continue;
|
||||
|
||||
pair.second += static_cast<int>(needDef->accumulationRate *
|
||||
deltaTime);
|
||||
pair.second += static_cast<int>(
|
||||
needDef->accumulationRate * deltaTime);
|
||||
if (pair.second > needDef->maxValue)
|
||||
pair.second = needDef->maxValue;
|
||||
if (pair.second < 0)
|
||||
@@ -222,9 +253,11 @@ void CharacterClassSystem::checkLevelUps()
|
||||
{
|
||||
m_world.query<CharacterIdentityComponent>().each(
|
||||
[&](flecs::entity e, CharacterIdentityComponent &ci) {
|
||||
auto *rec = CharacterRegistry::getSingleton().findCharacter(
|
||||
ci.registryId);
|
||||
if (!rec || rec->levelUpPending || rec->className.empty())
|
||||
auto *rec =
|
||||
CharacterRegistry::getSingleton().findCharacter(
|
||||
ci.registryId);
|
||||
if (!rec || rec->levelUpPending ||
|
||||
rec->className.empty())
|
||||
return;
|
||||
|
||||
const auto *cls = CharacterClassDatabase::getSingleton()
|
||||
@@ -232,8 +265,9 @@ void CharacterClassSystem::checkLevelUps()
|
||||
if (!cls)
|
||||
return;
|
||||
|
||||
int64_t needed = CharacterClassDatabase::getSingleton()
|
||||
.computeXPForLevel(rec->level, *cls);
|
||||
int64_t needed =
|
||||
CharacterClassDatabase::getSingleton()
|
||||
.computeXPForLevel(rec->level, *cls);
|
||||
if (rec->currentXP >= needed) {
|
||||
applyLevelUp(e);
|
||||
}
|
||||
@@ -251,8 +285,9 @@ void CharacterClassSystem::applyLevelUp(flecs::entity entity)
|
||||
if (!cls)
|
||||
return;
|
||||
|
||||
int64_t needed = CharacterClassDatabase::getSingleton().computeXPForLevel(
|
||||
rec->level, *cls);
|
||||
int64_t needed =
|
||||
CharacterClassDatabase::getSingleton().computeXPForLevel(
|
||||
rec->level, *cls);
|
||||
rec->currentXP -= needed;
|
||||
rec->level++;
|
||||
|
||||
@@ -273,8 +308,8 @@ void CharacterClassSystem::applyLevelUp(flecs::entity entity)
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
Ogre::String("CharacterClassSystem: ") +
|
||||
Ogre::String(entity.name()) +
|
||||
" reached level " + std::to_string(rec->level) + "!");
|
||||
Ogre::String(entity.name()) + " reached level " +
|
||||
std::to_string(rec->level) + "!");
|
||||
}
|
||||
|
||||
void CharacterClassSystem::applyLevelUpGrowthAndPoints(flecs::entity entity)
|
||||
@@ -298,10 +333,12 @@ void CharacterClassSystem::applyLevelUpGrowthAndPoints(flecs::entity entity)
|
||||
|
||||
// Auto-grow skills
|
||||
for (const auto &pair : cls->skillGrowth) {
|
||||
int growth = db.computeSkillGrowth(pair.first, rec->level, *cls);
|
||||
int growth =
|
||||
db.computeSkillGrowth(pair.first, rec->level, *cls);
|
||||
rec->skills[pair.first.c_str()] += growth;
|
||||
const auto *skillDef = db.findSkill(pair.first);
|
||||
if (skillDef && rec->skills[pair.first.c_str()] > skillDef->maxValue)
|
||||
if (skillDef &&
|
||||
rec->skills[pair.first.c_str()] > skillDef->maxValue)
|
||||
rec->skills[pair.first.c_str()] = skillDef->maxValue;
|
||||
}
|
||||
|
||||
@@ -325,16 +362,16 @@ void CharacterClassSystem::distributePointsAI(flecs::entity entity)
|
||||
// Round-robin primary stats
|
||||
int idx = 0;
|
||||
int safety = 0;
|
||||
while (points > 0 && !cls->primaryStats.empty() &&
|
||||
safety < 1000) {
|
||||
while (points > 0 && !cls->primaryStats.empty() && safety < 1000) {
|
||||
safety++;
|
||||
const auto &statName =
|
||||
cls->primaryStats[idx % cls->primaryStats.size()];
|
||||
int current = rec->stats.count(statName.c_str()) ?
|
||||
rec->stats[statName.c_str()] :
|
||||
0;
|
||||
int cost = CharacterClassDatabase::getSingleton()
|
||||
.computeStatCost(current, *cls);
|
||||
int cost =
|
||||
CharacterClassDatabase::getSingleton().computeStatCost(
|
||||
current, *cls);
|
||||
if (points >= cost) {
|
||||
rec->stats[statName.c_str()] = current + 1;
|
||||
points -= cost;
|
||||
@@ -399,7 +436,7 @@ void CharacterClassSystem::renderDialogs()
|
||||
std::vector<flecs::entity_t> closedSheets;
|
||||
for (flecs::entity_t id : m_sheets) {
|
||||
flecs::entity e = m_world.entity(id);
|
||||
if (!e.is_alive() || !e.has<CharacterIdentityComponent>()) {
|
||||
if (!e.is_alive()) {
|
||||
closedSheets.push_back(id);
|
||||
continue;
|
||||
}
|
||||
@@ -421,11 +458,10 @@ void CharacterClassSystem::renderLevelUpDialog(flecs::entity entity)
|
||||
return;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(200, 200), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(450, 500),
|
||||
ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(450, 500), ImGuiCond_FirstUseEver);
|
||||
|
||||
Ogre::String title = "Level Up! (Level " +
|
||||
std::to_string(rec->level) + ")";
|
||||
Ogre::String title =
|
||||
"Level Up! (Level " + std::to_string(rec->level) + ")";
|
||||
bool open = true;
|
||||
if (!ImGui::Begin(title.c_str(), &open)) {
|
||||
ImGui::End();
|
||||
@@ -481,77 +517,188 @@ void CharacterClassSystem::renderLevelUpDialog(flecs::entity entity)
|
||||
void CharacterClassSystem::renderCharacterSheet(flecs::entity entity)
|
||||
{
|
||||
auto *rec = getRecord(entity);
|
||||
if (!rec)
|
||||
return;
|
||||
|
||||
const auto *cls = CharacterClassDatabase::getSingleton().findClass(
|
||||
rec->className);
|
||||
const CharacterClassDatabase::ClassDef *cls = nullptr;
|
||||
if (rec)
|
||||
cls = CharacterClassDatabase::getSingleton().findClass(
|
||||
rec->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)) {
|
||||
// Full-screen window: use the entire display area
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
||||
ImGui::SetNextWindowSize(io.DisplaySize);
|
||||
|
||||
ImGuiWindowFlags flags =
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar;
|
||||
|
||||
if (!ImGui::Begin(title.c_str(), &open, flags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Text("Class: %s",
|
||||
cls ? cls->name.c_str() : rec->className.c_str());
|
||||
ImGui::Text("Level: %d", rec->level);
|
||||
ImGui::Text("XP: %ld", (long)rec->currentXP);
|
||||
ImGui::Text("Available Points: %d", rec->availablePoints);
|
||||
ImGui::Separator();
|
||||
if (rec) {
|
||||
ImGui::Text("Class: %s",
|
||||
cls ? cls->name.c_str() : rec->className.c_str());
|
||||
ImGui::Text("Level: %d", rec->level);
|
||||
ImGui::Text("XP: %ld", (long)rec->currentXP);
|
||||
ImGui::Text("Available Points: %d", rec->availablePoints);
|
||||
ImGui::Separator();
|
||||
|
||||
// Stats
|
||||
if (!rec->stats.empty()) {
|
||||
ImGui::Text("Stats");
|
||||
for (const auto &pair : rec->stats) {
|
||||
const auto *def = CharacterClassDatabase::getSingleton()
|
||||
.findStat(pair.first);
|
||||
if (def && def->kind ==
|
||||
CharacterClassDatabase::StatKind::ResourcePool) {
|
||||
int cur = getPoolCurrent(rec, pair.first);
|
||||
ImGui::Text(" %s: %d / %d", pair.first.c_str(),
|
||||
cur, pair.second);
|
||||
} else {
|
||||
// Stats
|
||||
if (!rec->stats.empty()) {
|
||||
ImGui::Text("Stats");
|
||||
for (const auto &pair : rec->stats) {
|
||||
const auto *def =
|
||||
CharacterClassDatabase::getSingleton()
|
||||
.findStat(pair.first);
|
||||
if (def &&
|
||||
def->kind ==
|
||||
CharacterClassDatabase::StatKind::
|
||||
ResourcePool) {
|
||||
int cur =
|
||||
getPoolCurrent(rec, pair.first);
|
||||
ImGui::Text(" %s: %d / %d",
|
||||
pair.first.c_str(), cur,
|
||||
pair.second);
|
||||
} else {
|
||||
ImGui::Text(" %s: %d",
|
||||
pair.first.c_str(),
|
||||
pair.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (!rec->skills.empty()) {
|
||||
ImGui::Text("Skills");
|
||||
for (const auto &pair : rec->skills) {
|
||||
ImGui::Text(" %s: %d", pair.first.c_str(),
|
||||
pair.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (!rec->skills.empty()) {
|
||||
ImGui::Text("Skills");
|
||||
for (const auto &pair : rec->skills) {
|
||||
ImGui::Text(" %s: %d", pair.first.c_str(),
|
||||
pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
// Needs
|
||||
if (!rec->needs.empty()) {
|
||||
ImGui::Text("Needs");
|
||||
for (const auto &pair : rec->needs) {
|
||||
ImGui::Text(" %s: %d", pair.first.c_str(),
|
||||
pair.second);
|
||||
// Needs
|
||||
if (!rec->needs.empty()) {
|
||||
ImGui::Text("Needs");
|
||||
for (const auto &pair : rec->needs) {
|
||||
ImGui::Text(" %s: %d", pair.first.c_str(),
|
||||
pair.second);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ImGui::Text("(No character data available)");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Inventory placeholder
|
||||
// Inventory
|
||||
if (entity.has<InventoryComponent>()) {
|
||||
ImGui::Text("Inventory available (not yet displayed)");
|
||||
auto &inv = entity.get<InventoryComponent>();
|
||||
ImGui::Text("Inventory (%.1f / %.1f kg)", inv.totalWeight,
|
||||
inv.maxWeight);
|
||||
ImGui::Separator();
|
||||
if (inv.slots.empty()) {
|
||||
ImGui::Text(" (empty)");
|
||||
} else {
|
||||
for (size_t i = 0; i < inv.slots.size(); i++) {
|
||||
const auto &slot = inv.slots[i];
|
||||
if (slot.isEmpty())
|
||||
continue;
|
||||
Ogre::String itemName =
|
||||
ItemRegistry::getSingleton().getItemName(
|
||||
slot.itemId);
|
||||
if (itemName.empty())
|
||||
itemName = slot.itemId;
|
||||
ImGui::Text(" %s x%d", itemName.c_str(),
|
||||
slot.stackSize);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ImGui::Text("No inventory");
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
if (!open)
|
||||
if (!open) {
|
||||
m_sheets.erase(entity.id());
|
||||
// Unpause the game when the sheet is closed via the X button
|
||||
if (m_editorApp &&
|
||||
m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
|
||||
m_editorApp->getGamePlayState() ==
|
||||
EditorApp::GamePlayState::Paused) {
|
||||
m_editorApp->setGamePlayState(
|
||||
EditorApp::GamePlayState::Playing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// InventoryDialogConfig implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void InventoryDialogConfig::prepareFont(EditorApp *editorApp)
|
||||
{
|
||||
if (!editorApp)
|
||||
return;
|
||||
|
||||
Ogre::ImGuiOverlay *overlay = editorApp->getImGuiOverlay();
|
||||
if (!overlay)
|
||||
return;
|
||||
|
||||
if (fontPath.empty())
|
||||
return;
|
||||
|
||||
try {
|
||||
Ogre::String fontName = "InventoryDialogFont";
|
||||
if (Ogre::FontManager::getSingleton().resourceExists(
|
||||
fontName, "General")) {
|
||||
Ogre::FontManager::getSingleton().remove(fontName,
|
||||
"General");
|
||||
}
|
||||
Ogre::FontPtr font = Ogre::FontManager::getSingleton().create(
|
||||
fontName, "General");
|
||||
font->setType(Ogre::FontType::FT_TRUETYPE);
|
||||
font->setSource(fontPath);
|
||||
font->setTrueTypeSize(fontSize);
|
||||
font->setTrueTypeResolution(75);
|
||||
font->addCodePointRange(Ogre::Font::CodePointRange(32, 255));
|
||||
font->load();
|
||||
overlay->addFont(fontName, "General");
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"InventoryDialogConfig: Failed to load font " +
|
||||
fontPath);
|
||||
}
|
||||
}
|
||||
|
||||
bool InventoryDialogConfig::loadFromFile(const std::string &filepath)
|
||||
{
|
||||
std::ifstream f(filepath);
|
||||
if (!f.is_open())
|
||||
return false;
|
||||
try {
|
||||
nlohmann::json j;
|
||||
f >> j;
|
||||
deserialize(j);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool InventoryDialogConfig::saveToFile(const std::string &filepath)
|
||||
{
|
||||
try {
|
||||
std::ofstream f(filepath);
|
||||
if (!f.is_open())
|
||||
return false;
|
||||
f << serialize().dump(2);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,45 @@
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <unordered_set>
|
||||
#include <string>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
* Configuration for the character sheet / inventory dialog.
|
||||
* Loaded from / saved to inventory_config.json.
|
||||
*/
|
||||
struct InventoryDialogConfig {
|
||||
std::string fontPath = "";
|
||||
float fontSize = 16.0f;
|
||||
|
||||
nlohmann::json serialize() const
|
||||
{
|
||||
nlohmann::json j;
|
||||
j["fontPath"] = fontPath;
|
||||
j["fontSize"] = fontSize;
|
||||
return j;
|
||||
}
|
||||
|
||||
void deserialize(const nlohmann::json &j)
|
||||
{
|
||||
if (j.contains("fontPath") && j["fontPath"].is_string())
|
||||
fontPath = j["fontPath"].get<std::string>();
|
||||
if (j.contains("fontSize") && j["fontSize"].is_number())
|
||||
fontSize = j["fontSize"].get<float>();
|
||||
}
|
||||
|
||||
bool loadFromFile(const std::string &filepath);
|
||||
bool saveToFile(const std::string &filepath);
|
||||
|
||||
/**
|
||||
* Load the configured font into ImGui via Ogre's ImGuiOverlay.
|
||||
* Should be called once during setup, before the overlay is shown.
|
||||
*/
|
||||
void prepareFont(class EditorApp *editorApp);
|
||||
};
|
||||
|
||||
/**
|
||||
* System that manages character progression, need accumulation,
|
||||
* and level-up logic.
|
||||
@@ -36,7 +72,29 @@ public:
|
||||
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);
|
||||
bool distributePoint(flecs::entity entity,
|
||||
const Ogre::String &statName);
|
||||
|
||||
/** Toggle character sheet for the given entity. */
|
||||
void toggleCharacterSheet(flecs::entity entity);
|
||||
|
||||
/** Check if the character sheet is currently open for any entity. */
|
||||
bool isSheetOpen() const
|
||||
{
|
||||
return !m_sheets.empty();
|
||||
}
|
||||
|
||||
/** Get the inventory dialog configuration. */
|
||||
InventoryDialogConfig &getConfig()
|
||||
{
|
||||
return m_config;
|
||||
}
|
||||
|
||||
/** Set the inventory dialog configuration (from editor UI). */
|
||||
void setConfig(const InventoryDialogConfig &cfg)
|
||||
{
|
||||
m_config = cfg;
|
||||
}
|
||||
|
||||
private:
|
||||
void accumulateNeeds(float deltaTime);
|
||||
@@ -55,6 +113,9 @@ private:
|
||||
std::unordered_set<flecs::entity_t> m_levelUpDialogs;
|
||||
// Track which entities have an open character sheet
|
||||
std::unordered_set<flecs::entity_t> m_sheets;
|
||||
|
||||
// Inventory dialog configuration
|
||||
InventoryDialogConfig m_config;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CHARACTER_CLASS_SYSTEM_HPP
|
||||
|
||||
@@ -301,9 +301,13 @@ void CharacterSystem::update(float deltaTime)
|
||||
* writes position back to scene node normally. */
|
||||
|
||||
/* Apply velocity via Jolt linear velocity.
|
||||
* Preserve physics-driven Y velocity when no explicit
|
||||
* vertical input is given so gravity/buoyancy/jumps
|
||||
* are not overwritten every frame. */
|
||||
* For a dynamic character body, preserving the
|
||||
* physics-driven Y velocity when on the ground
|
||||
* causes gravity to continuously pull the character
|
||||
* into the floor. The collision solver pushes back,
|
||||
* but the body never settles, producing visible
|
||||
* micro-jitter. Zero out Y when supported and no
|
||||
* explicit vertical input is requested. */
|
||||
JPH::Vec3 currentVel =
|
||||
state.character->GetLinearVelocity();
|
||||
JPH::Vec3 desiredVel = JoltPhysics::convert<JPH::Vec3>(
|
||||
@@ -311,16 +315,15 @@ void CharacterSystem::update(float deltaTime)
|
||||
JPH::Vec3 finalVel;
|
||||
finalVel.SetX(desiredVel.GetX());
|
||||
finalVel.SetZ(desiredVel.GetZ());
|
||||
finalVel.SetY(desiredVel.GetY() != 0.0f ?
|
||||
desiredVel.GetY() :
|
||||
currentVel.GetY());
|
||||
state.character->SetLinearVelocity(finalVel);
|
||||
if (cc.linearVelocity.squaredLength() > 0.0001f) {
|
||||
std::cout << "CharacterSystem::update: entity="
|
||||
<< e.id()
|
||||
<< " vel=" << cc.linearVelocity
|
||||
<< std::endl;
|
||||
if (desiredVel.GetY() != 0.0f) {
|
||||
finalVel.SetY(desiredVel.GetY());
|
||||
} else if (state.character->IsSupported()) {
|
||||
finalVel.SetY(0.0f);
|
||||
} else {
|
||||
finalVel.SetY(currentVel.GetY());
|
||||
}
|
||||
state.character->SetLinearVelocity(finalVel);
|
||||
|
||||
|
||||
/* Floor detection: raycast downward to find ground */
|
||||
if (cc.useGravity && !cc.hasFloor) {
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "../components/PrefabInstance.hpp"
|
||||
#include "../components/Item.hpp"
|
||||
#include "../components/Inventory.hpp"
|
||||
#include "CharacterClassSystem.hpp"
|
||||
|
||||
#include "../ui/TransformEditor.hpp"
|
||||
#include "../ui/RenderableEditor.hpp"
|
||||
@@ -296,9 +297,11 @@ void EditorUISystem::registerComponentEditors()
|
||||
});
|
||||
|
||||
// Register CharacterIdentity component
|
||||
auto characterIdentityEditor = std::make_unique<CharacterIdentityEditor>();
|
||||
auto characterIdentityEditor =
|
||||
std::make_unique<CharacterIdentityEditor>();
|
||||
m_componentRegistry.registerComponent<CharacterIdentityComponent>(
|
||||
"Character Identity", "Character", std::move(characterIdentityEditor),
|
||||
"Character Identity", "Character",
|
||||
std::move(characterIdentityEditor),
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<CharacterIdentityComponent>())
|
||||
e.set<CharacterIdentityComponent>(
|
||||
@@ -378,6 +381,11 @@ void EditorUISystem::update(float deltaTime)
|
||||
ItemRegistry::getSingleton().drawEditor(&m_showItemRegistry);
|
||||
}
|
||||
|
||||
// Render Inventory Dialog Config window
|
||||
if (m_showInventoryConfig) {
|
||||
renderInventoryConfigWindow();
|
||||
}
|
||||
|
||||
// Render FPS overlay
|
||||
renderFPSOverlay(deltaTime);
|
||||
}
|
||||
@@ -475,13 +483,17 @@ void EditorUISystem::renderHierarchyWindow()
|
||||
"Character Class Database")) {
|
||||
m_showCharacterClassDatabase = true;
|
||||
}
|
||||
if (ImGui::MenuItem(
|
||||
"Character Registry")) {
|
||||
if (ImGui::MenuItem("Character Registry")) {
|
||||
m_showCharacterRegistry = true;
|
||||
}
|
||||
if (ImGui::MenuItem("Item Registry")) {
|
||||
m_showItemRegistry = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(
|
||||
"Inventory Dialog Config")) {
|
||||
m_showInventoryConfig = true;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
@@ -623,6 +635,64 @@ void EditorUISystem::renderHierarchyWindow()
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void EditorUISystem::renderInventoryConfigWindow()
|
||||
{
|
||||
ImGui::SetNextWindowPos(ImVec2(LEFT_PANEL_WIDTH, 100),
|
||||
ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(350, 200), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
if (!ImGui::Begin("Inventory Dialog Config", &m_showInventoryConfig,
|
||||
flags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"Configure the character sheet / inventory dialog font.");
|
||||
ImGui::TextWrapped("Settings are persisted to inventory_config.json.");
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Local copy for editing, loaded from file
|
||||
static InventoryDialogConfig editConfig;
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
editConfig.loadFromFile("inventory_config.json");
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
char fontPathBuf[256];
|
||||
std::strncpy(fontPathBuf, editConfig.fontPath.c_str(),
|
||||
sizeof(fontPathBuf) - 1);
|
||||
fontPathBuf[sizeof(fontPathBuf) - 1] = '\0';
|
||||
if (ImGui::InputText("Font Path", fontPathBuf, sizeof(fontPathBuf))) {
|
||||
editConfig.fontPath = fontPathBuf;
|
||||
}
|
||||
|
||||
if (ImGui::DragFloat("Font Size", &editConfig.fontSize, 0.5f, 8.0f,
|
||||
72.0f)) {
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Button("Save to inventory_config.json")) {
|
||||
if (editConfig.saveToFile("inventory_config.json")) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Inventory dialog config saved.");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Load from inventory_config.json")) {
|
||||
if (editConfig.loadFromFile("inventory_config.json")) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Inventory dialog config loaded.");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void EditorUISystem::renderEntityNode(flecs::entity entity, int depth)
|
||||
{
|
||||
if (!entity.is_alive())
|
||||
@@ -1005,7 +1075,8 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
// Render CharacterIdentity if present
|
||||
if (entity.has<CharacterIdentityComponent>()) {
|
||||
auto &ci = entity.get_mut<CharacterIdentityComponent>();
|
||||
m_componentRegistry.render<CharacterIdentityComponent>(entity, ci);
|
||||
m_componentRegistry.render<CharacterIdentityComponent>(entity,
|
||||
ci);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
@@ -1199,8 +1270,6 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Show message if no components
|
||||
|
||||
if (componentCount == 0) {
|
||||
@@ -2143,7 +2212,6 @@ void EditorUISystem::renderCursorPanel()
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
|
||||
void EditorUISystem::renderDialogueSettingsWindow()
|
||||
{
|
||||
ImGui::SetNextWindowPos(ImVec2(LEFT_PANEL_WIDTH, 100),
|
||||
@@ -2167,25 +2235,23 @@ void EditorUISystem::renderDialogueSettingsWindow()
|
||||
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))) {
|
||||
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("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::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("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::SliderFloat("Box Position Fraction", &s.boxPositionFraction,
|
||||
0.0f, 1.0f)) {
|
||||
}
|
||||
|
||||
if (ImGui::Button("Apply Settings")) {
|
||||
@@ -2216,8 +2282,8 @@ void EditorUISystem::renderDialogueSettingsWindow()
|
||||
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::InputTextMultiline("Sample Text", sampleText, sizeof(sampleText),
|
||||
ImVec2(0, 60));
|
||||
ImGui::InputText("Sample Speaker", sampleSpeaker,
|
||||
sizeof(sampleSpeaker));
|
||||
ImGui::InputTextMultiline("Sample Choices (one per line)",
|
||||
|
||||
@@ -194,6 +194,7 @@ public:
|
||||
void renderPrefabBrowser();
|
||||
void renderCursorPanel();
|
||||
void renderDialogueSettingsWindow();
|
||||
void renderInventoryConfigWindow();
|
||||
|
||||
private:
|
||||
// File menu
|
||||
@@ -308,6 +309,9 @@ private:
|
||||
// Dialogue settings editor state
|
||||
bool m_showDialogueSettings = false;
|
||||
|
||||
// Inventory config editor state
|
||||
bool m_showInventoryConfig = false;
|
||||
|
||||
// Character class database editor state
|
||||
bool m_showCharacterClassDatabase = false;
|
||||
CharacterClassDatabaseEditor m_characterClassDatabaseEditor;
|
||||
|
||||
@@ -190,11 +190,9 @@ void PlayerControllerSystem::updateTPSCamera(PlayerControllerComponent &pc,
|
||||
|
||||
state.goalNode->setPosition(goalPos);
|
||||
|
||||
// Smoothly interpolate camera to goal
|
||||
Ogre::Vector3 currentPos = camNode->getPosition();
|
||||
Ogre::Vector3 newPos =
|
||||
Ogre::Math::lerp(currentPos, goalPos, deltaTime * 9.0f);
|
||||
camNode->setPosition(newPos);
|
||||
// Snap camera directly to goal to avoid jitter
|
||||
// caused by lerp smoothing with varying dt.
|
||||
camNode->setPosition(goalPos);
|
||||
camNode->lookAt(pivotPos, Ogre::Node::TS_WORLD);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user