No more animation jitter

This commit is contained in:
2026-05-18 16:01:53 +03:00
parent 3f40d84847
commit bea438bd50
12 changed files with 796 additions and 217 deletions
+205 -27
View File
@@ -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));
+3
View File
@@ -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;
}
};
+13 -1
View File
@@ -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);
}