diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index eec2b78..6fc88fc 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -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( m_world, m_sceneMgr, this, m_behaviorTreeSystem.get()); - // Setup Event Handler system - m_eventHandlerSystem = std::make_unique( - m_world, m_behaviorTreeSystem.get()); - // Setup Item system m_itemSystem = std::make_unique( 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( + 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( - m_world, this); - m_pregnancySystem = - std::make_unique(m_world); - CharacterClassDatabase::loadFromJson( - "character_class.json"); + std::make_unique(m_world, this); + m_pregnancySystem = std::make_unique(m_world); + CharacterClassDatabase::loadFromJson("character_class.json"); m_playerControllerSystem = std::make_unique( @@ -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().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()) { + auto &pc = playerController.get(); + if (!pc.targetCharacterName.empty()) { + m_world.query().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()) { + // 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{ 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()) { + playerCharacter.set( + 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(); m_world.component(); - // Register environment components m_world.component(); m_world.component(); @@ -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().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()) { + auto &pc = playerController.get(); + if (!pc.targetCharacterName.empty()) { + m_world.query().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)); diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index 0de9739..33d2439 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -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; } }; diff --git a/src/features/editScene/physics/physics.cpp b/src/features/editScene/physics/physics.cpp index 4eb424a..22d3659 100644 --- a/src/features/editScene/physics/physics.cpp +++ b/src/features/editScene/physics/physics.cpp @@ -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(), diff --git a/src/features/editScene/systems/ActuatorSystem.cpp b/src/features/editScene/systems/ActuatorSystem.cpp index a08ace7..f8b93ad 100644 --- a/src/features/editScene/systems/ActuatorSystem.cpp +++ b/src/features/editScene/systems/ActuatorSystem.cpp @@ -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); } diff --git a/src/features/editScene/systems/AnimationTreeSystem.cpp b/src/features/editScene/systems/AnimationTreeSystem.cpp index d4ae7e4..8ceaa3f 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.cpp +++ b/src/features/editScene/systems/AnimationTreeSystem.cpp @@ -9,6 +9,19 @@ #include #include +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> + 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()) { - auto &cc = e.get_mut(); - 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()) { + auto &cc = e.get_mut(); + 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); diff --git a/src/features/editScene/systems/AnimationTreeSystem.hpp b/src/features/editScene/systems/AnimationTreeSystem.hpp index dd338ca..c3ae866 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.hpp +++ b/src/features/editScene/systems/AnimationTreeSystem.hpp @@ -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 animations; std::unordered_map fadeStates; diff --git a/src/features/editScene/systems/CharacterClassSystem.cpp b/src/features/editScene/systems/CharacterClassSystem.cpp index a762fd4..e9ec116 100644 --- a/src/features/editScene/systems/CharacterClassSystem.cpp +++ b/src/features/editScene/systems/CharacterClassSystem.cpp @@ -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 +#include +#include #include #include +#include /* --------------------------------------------------------------------------- * 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().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(needDef->accumulationRate * - deltaTime); + pair.second += static_cast( + 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().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 closedSheets; for (flecs::entity_t id : m_sheets) { flecs::entity e = m_world.entity(id); - if (!e.is_alive() || !e.has()) { + 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()) { - ImGui::Text("Inventory available (not yet displayed)"); + auto &inv = entity.get(); + 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; + } } diff --git a/src/features/editScene/systems/CharacterClassSystem.hpp b/src/features/editScene/systems/CharacterClassSystem.hpp index 243668b..fb3f72a 100644 --- a/src/features/editScene/systems/CharacterClassSystem.hpp +++ b/src/features/editScene/systems/CharacterClassSystem.hpp @@ -5,9 +5,45 @@ #include #include #include +#include +#include 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(); + if (j.contains("fontSize") && j["fontSize"].is_number()) + fontSize = j["fontSize"].get(); + } + + 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 m_levelUpDialogs; // Track which entities have an open character sheet std::unordered_set m_sheets; + + // Inventory dialog configuration + InventoryDialogConfig m_config; }; #endif // EDITSCENE_CHARACTER_CLASS_SYSTEM_HPP diff --git a/src/features/editScene/systems/CharacterSystem.cpp b/src/features/editScene/systems/CharacterSystem.cpp index 806b4c2..7071477 100644 --- a/src/features/editScene/systems/CharacterSystem.cpp +++ b/src/features/editScene/systems/CharacterSystem.cpp @@ -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( @@ -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) { diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 6905bdd..f6b3b0c 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -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(); + auto characterIdentityEditor = + std::make_unique(); m_componentRegistry.registerComponent( - "Character Identity", "Character", std::move(characterIdentityEditor), + "Character Identity", "Character", + std::move(characterIdentityEditor), [](flecs::entity e) { if (!e.has()) e.set( @@ -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()) { auto &ci = entity.get_mut(); - m_componentRegistry.render(entity, ci); + m_componentRegistry.render(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)", diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index c25fc98..7aeeb8c 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -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; diff --git a/src/features/editScene/systems/PlayerControllerSystem.cpp b/src/features/editScene/systems/PlayerControllerSystem.cpp index f9ecea2..dd29f91 100644 --- a/src/features/editScene/systems/PlayerControllerSystem.cpp +++ b/src/features/editScene/systems/PlayerControllerSystem.cpp @@ -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); }