From cebca9a88223e83a549875ffb2aeaf152fa43b8b Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 08:27:32 -0700 Subject: [PATCH] fix(gameplay): stabilize run animation and clean ghost/death visuals --- include/game/game_handler.hpp | 1 + include/rendering/renderer.hpp | 4 +++ src/core/application.cpp | 4 ++- src/game/game_handler.cpp | 15 ++++++++-- src/rendering/renderer.cpp | 50 +++++++++++++++++++--------------- src/ui/game_screen.cpp | 4 ++- 6 files changed, 51 insertions(+), 27 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 04e959db..52a6f1a0 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -3201,6 +3201,7 @@ private: bool releasedSpirit_ = false; uint32_t corpseMapId_ = 0; float corpseX_ = 0.0f, corpseY_ = 0.0f, corpseZ_ = 0.0f; + uint64_t corpseGuid_ = 0; // Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially std::array playerRunes_ = [] { std::array r{}; diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index dd727caf..05e55219 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -340,6 +340,10 @@ private: // Character animation state enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE }; CharAnimState charAnimState = CharAnimState::IDLE; + float locomotionStopGraceTimer_ = 0.0f; + bool locomotionWasSprinting_ = false; + uint32_t lastPlayerAnimRequest_ = UINT32_MAX; + bool lastPlayerAnimLoopRequest_ = true; void updateCharacterAnimation(); bool isFootstepAnimationState() const; bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs); diff --git a/src/core/application.cpp b/src/core/application.cpp index 11c4a0ed..9d0a30e1 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -3081,9 +3081,11 @@ void Application::setupUICallbacks() { if (charInstId == 0) return; // WoW stand state → M2 animation ID mapping // 0=Stand→0, 1-6=Sit variants→27 (SitGround), 7=Dead→1, 8=Kneel→72 + // Do not force Stand(0) here: locomotion state machine already owns standing/running. + // Forcing Stand on packet timing causes visible run-cycle hitching while steering. uint32_t animId = 0; if (standState == 0) { - animId = 0; // Stand + return; } else if (standState >= 1 && standState <= 6) { animId = 27; // SitGround (covers sit-chair too; correct visual differs by chair height) } else if (standState == 7) { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 0d933601..c96ef05d 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -8506,6 +8506,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { castTimeTotal = 0.0f; playerDead_ = false; releasedSpirit_ = false; + corpseGuid_ = 0; targetGuid = 0; focusGuid = 0; lastTargetGuid = 0; @@ -9963,6 +9964,7 @@ void GameHandler::forceClearTaxiAndMovementState() { resurrectRequestPending_ = false; playerDead_ = false; releasedSpirit_ = false; + corpseGuid_ = 0; repopPending_ = false; pendingSpiritHealerGuid_ = 0; resurrectCasterGuid_ = 0; @@ -10580,11 +10582,13 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { uint64_t ownerGuid = (static_cast(ownerHigh) << 32) | ownerLow; if (ownerGuid == playerGuid || ownerLow == static_cast(playerGuid)) { // Server coords from movement block + corpseGuid_ = block.guid; corpseX_ = block.x; corpseY_ = block.y; corpseZ_ = block.z; corpseMapId_ = currentMapId_; - LOG_INFO("Corpse object detected: server=(", block.x, ", ", block.y, ", ", block.z, + LOG_INFO("Corpse object detected: guid=0x", std::hex, corpseGuid_, std::dec, + " server=(", block.x, ", ", block.y, ", ", block.z, ") map=", corpseMapId_); } } @@ -11136,6 +11140,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { repopPending_ = false; resurrectPending_ = false; corpseMapId_ = 0; // corpse reclaimed + corpseGuid_ = 0; LOG_INFO("Player resurrected (PLAYER_FLAGS ghost cleared)"); if (ghostStateCallback_) ghostStateCallback_(false); } @@ -12908,9 +12913,12 @@ bool GameHandler::canReclaimCorpse() const { void GameHandler::reclaimCorpse() { if (!canReclaimCorpse() || !socket) return; - auto packet = ReclaimCorpsePacket::build(playerGuid); + // Reclaim expects the corpse object guid when known; fallback to player guid. + uint64_t reclaimGuid = (corpseGuid_ != 0) ? corpseGuid_ : playerGuid; + auto packet = ReclaimCorpsePacket::build(reclaimGuid); socket->send(packet); - LOG_INFO("Sent CMSG_RECLAIM_CORPSE for guid=0x", std::hex, playerGuid, std::dec); + LOG_INFO("Sent CMSG_RECLAIM_CORPSE for guid=0x", std::hex, reclaimGuid, std::dec, + (corpseGuid_ == 0 ? " (fallback player guid)" : "")); } void GameHandler::activateSpiritHealer(uint64_t npcGuid) { @@ -20420,6 +20428,7 @@ void GameHandler::handleNewWorld(network::Packet& packet) { pendingSpiritHealerGuid_ = 0; resurrectCasterGuid_ = 0; corpseMapId_ = 0; + corpseGuid_ = 0; hostileAttackers_.clear(); stopAutoAttack(); tabCycleStale = true; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index e1de4a01..c505e5d9 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -1031,9 +1031,8 @@ void Renderer::beginFrame() { // FXAA resource management — FXAA can coexist with FSR1 and FSR3. // When both FXAA and FSR3 are enabled, FXAA runs as a post-FSR3 pass. - // Ghost mode also reuses this post pass for true grayscale when FXAA is - // disabled in settings. - const bool useFXAAPostPass = (fxaa_.enabled || ghostMode_); + // Do not force this pass for ghost mode; keep AA quality strictly user-controlled. + const bool useFXAAPostPass = fxaa_.enabled; if ((fxaa_.needsRecreate || !useFXAAPostPass) && fxaa_.sceneFramebuffer) { destroyFXAAResources(); fxaa_.needsRecreate = false; @@ -1250,7 +1249,7 @@ void Renderer::endFrame() { // FSR3+FXAA combined: re-point FXAA's descriptor to the FSR3 temporal output // so renderFXAAPass() applies spatial AA on the temporally-stabilized frame. // This must happen outside the render pass (descriptor updates are CPU-side). - if ((fxaa_.enabled || ghostMode_) && fxaa_.descSet && fxaa_.sceneSampler) { + if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler) { VkImageView fsr3OutputView = VK_NULL_HANDLE; if (fsr2_.useAmdBackend) { if (fsr2_.amdFsr3FramegenRuntimeActive && fsr2_.framegenOutput.image) @@ -1309,7 +1308,7 @@ void Renderer::endFrame() { // of RCAS sharpening. FXAA descriptor is temporarily pointed to the FSR3 // history buffer (which is already in SHADER_READ_ONLY_OPTIMAL). This gives // FSR3 temporal stability + FXAA spatial edge smoothing ("ultra quality native"). - if ((fxaa_.enabled || ghostMode_) && fxaa_.pipeline && fxaa_.descSet) { + if (fxaa_.enabled && fxaa_.pipeline && fxaa_.descSet) { renderFXAAPass(); } else { // Draw RCAS sharpening from accumulated history buffer @@ -1318,7 +1317,7 @@ void Renderer::endFrame() { // Restore FXAA descriptor to its normal scene color source so standalone // FXAA frames are not affected by the FSR3 history pointer set above. - if ((fxaa_.enabled || ghostMode_) && fxaa_.descSet && fxaa_.sceneSampler && fxaa_.sceneColor.imageView) { + if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler && fxaa_.sceneColor.imageView) { VkDescriptorImageInfo restoreInfo{}; restoreInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; restoreInfo.imageView = fxaa_.sceneColor.imageView; @@ -1342,7 +1341,7 @@ void Renderer::endFrame() { } fsr2_.frameIndex = (fsr2_.frameIndex + 1) % 256; // Wrap to keep Halton values well-distributed - } else if ((fxaa_.enabled || ghostMode_) && fxaa_.sceneFramebuffer) { + } else if (fxaa_.enabled && fxaa_.sceneFramebuffer) { // End the off-screen scene render pass vkCmdEndRenderPass(currentCmd); @@ -1857,7 +1856,18 @@ void Renderer::updateCharacterAnimation() { CharAnimState newState = charAnimState; - bool moving = cameraController->isMoving(); + const bool rawMoving = cameraController->isMoving(); + const bool rawSprinting = cameraController->isSprinting(); + constexpr float kLocomotionStopGraceSec = 0.12f; + if (rawMoving) { + locomotionStopGraceTimer_ = kLocomotionStopGraceSec; + locomotionWasSprinting_ = rawSprinting; + } else { + locomotionStopGraceTimer_ = std::max(0.0f, locomotionStopGraceTimer_ - lastDeltaTime_); + } + // Debounce brief input/state dropouts (notably during both-mouse steering) so + // locomotion clips do not restart every few frames. + bool moving = rawMoving || locomotionStopGraceTimer_ > 0.0f; bool movingForward = cameraController->isMovingForward(); bool movingBackward = cameraController->isMovingBackward(); bool autoRunning = cameraController->isAutoRunning(); @@ -1870,7 +1880,7 @@ void Renderer::updateCharacterAnimation() { bool anyStrafeRight = strafeRight && !strafeLeft && pureStrafe; bool grounded = cameraController->isGrounded(); bool jumping = cameraController->isJumping(); - bool sprinting = cameraController->isSprinting(); + bool sprinting = rawSprinting || (!rawMoving && moving && locomotionWasSprinting_); bool sitting = cameraController->isSitting(); bool swim = cameraController->isSwimming(); bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim; @@ -2530,8 +2540,14 @@ void Renderer::updateCharacterAnimation() { float currentAnimTimeMs = 0.0f; float currentAnimDurationMs = 0.0f; bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); - if (!haveState || currentAnimId != animId) { + // Some frames may transiently fail getAnimationState() while resources/instance state churn. + // Avoid reissuing the same clip on those frames, which restarts locomotion and causes hitches. + const bool requestChanged = (lastPlayerAnimRequest_ != animId) || (lastPlayerAnimLoopRequest_ != loop); + const bool shouldPlay = (haveState && currentAnimId != animId) || (!haveState && requestChanged); + if (shouldPlay) { characterRenderer->playAnimation(characterInstanceId, animId, loop); + lastPlayerAnimRequest_ = animId; + lastPlayerAnimLoopRequest_ = loop; } } @@ -5075,7 +5091,7 @@ void Renderer::renderFXAAPass() { vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr); - // Pass rcpFrame + sharpness + desaturate (vec4, 16 bytes). + // Pass rcpFrame + sharpness + effect flag (vec4, 16 bytes). // When FSR2/FSR3 is active alongside FXAA, forward FSR2's sharpness so the // post-FXAA unsharp-mask step restores the crispness that FXAA's blur removes. float sharpness = fsr2_.enabled ? fsr2_.sharpness : 0.0f; @@ -5083,7 +5099,7 @@ void Renderer::renderFXAAPass() { 1.0f / static_cast(ext.width), 1.0f / static_cast(ext.height), sharpness, - ghostMode_ ? 1.0f : 0.0f // desaturate: 1=ghost grayscale, 0=normal + 0.0f }; vkCmdPushConstants(currentCmd, fxaa_.pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, pc); @@ -5273,12 +5289,6 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { renderOverlay(tint, cmd); } } - // Ghost mode desaturation overlay (non-FXAA path approximation). - // When FXAA is active the FXAA shader applies true per-pixel desaturation; - // otherwise a high-opacity gray overlay gives a similar washed-out effect. - if (ghostMode_ && overlayPipeline && !(fxaa_.enabled || fxaa_.sceneFramebuffer)) { - renderOverlay(glm::vec4(0.5f, 0.5f, 0.55f, 0.82f), cmd); - } if (minimap && minimap->isEnabled() && camera && window) { glm::vec3 minimapCenter = camera->getPosition(); if (cameraController && cameraController->isThirdPerson()) @@ -5413,10 +5423,6 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { renderOverlay(tint); } } - // Ghost mode desaturation overlay (non-FXAA path approximation). - if (ghostMode_ && overlayPipeline && !(fxaa_.enabled || fxaa_.sceneFramebuffer)) { - renderOverlay(glm::vec4(0.5f, 0.5f, 0.55f, 0.82f)); - } if (minimap && minimap->isEnabled() && camera && window) { glm::vec3 minimapCenter = camera->getPosition(); if (cameraController && cameraController->isThirdPerson()) diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index dbd63505..c9be2a9a 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -845,7 +845,9 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Update renderer face-target position and selection circle auto* renderer = core::Application::getInstance().getRenderer(); if (renderer) { - renderer->setInCombat(gameHandler.isInCombat()); + renderer->setInCombat(gameHandler.isInCombat() && + !gameHandler.isPlayerDead() && + !gameHandler.isPlayerGhost()); static glm::vec3 targetGLPos; if (gameHandler.hasTarget()) { auto target = gameHandler.getTarget();