From c622fde7be6300fccd856c872a2a3f3d5b22cd6b Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 12:28:11 -0700 Subject: [PATCH] physics: implement knockback simulation from SMSG_MOVE_KNOCK_BACK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the handler ACKed with current position and ignored the velocity fields entirely (vcos/vsin/hspeed/vspeed were [[maybe_unused]]). The server expects the client to fly through the air on knockback — without simulation the player stays in place while the server models them as airborne, causing position desync and rubberbanding. Changes: - CameraController: add applyKnockBack(vcos, vsin, hspeed, vspeed) that sets knockbackHorizVel_ and launches verticalVelocity = -vspeed (server sends vspeed as negative for upward launches, matching TrinityCore) - Physics loop: each tick adds knockbackHorizVel_ to targetPos then applies exponential drag (KNOCKBACK_HORIZ_DRAG=4.5/s) until velocity < 0.05 u/s - GameHandler: parse all four fields, add KnockBackCallback, call it for the local player so the camera controller receives the impulse - Application: register the callback — routes server knockback to physics The existing ACK path is unchanged; the server gets position confirmation as before while the client now actually simulates the trajectory. --- include/game/game_handler.hpp | 6 +++++ include/rendering/camera_controller.hpp | 14 ++++++++++++ src/core/application.cpp | 5 +++++ src/game/game_handler.cpp | 17 +++++++++----- src/rendering/camera_controller.cpp | 30 +++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 166d2573..b34bc1a9 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -693,6 +693,11 @@ public: using WorldEntryCallback = std::function; void setWorldEntryCallback(WorldEntryCallback cb) { worldEntryCallback_ = std::move(cb); } + // Knockback callback: called when server sends SMSG_MOVE_KNOCK_BACK for the player. + // Parameters: vcos, vsin (render-space direction), hspeed, vspeed (raw from packet). + using KnockBackCallback = std::function; + void setKnockBackCallback(KnockBackCallback cb) { knockBackCallback_ = std::move(cb); } + // Unstuck callback (resets player Z to floor height) using UnstuckCallback = std::function; void setUnstuckCallback(UnstuckCallback cb) { unstuckCallback_ = std::move(cb); } @@ -1812,6 +1817,7 @@ private: // ---- Phase 3: Spells ---- WorldEntryCallback worldEntryCallback_; + KnockBackCallback knockBackCallback_; UnstuckCallback unstuckCallback_; UnstuckCallback unstuckGyCallback_; UnstuckCallback unstuckHearthCallback_; diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 79a7d622..679b2fa4 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -103,6 +103,12 @@ public: // Trigger mount jump (applies vertical velocity for physics hop) void triggerMountJump(); + // Apply server-driven knockback impulse. + // dir: render-space 2D direction unit vector (from vcos/vsin in packet) + // hspeed: horizontal speed magnitude (units/s) + // vspeed: raw packet vspeed field (server sends negative for upward launch) + void applyKnockBack(float vcos, float vsin, float hspeed, float vspeed); + // For first-person player hiding void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) { characterRenderer = cr; @@ -313,6 +319,14 @@ private: float cachedFloorHeight_ = 0.0f; bool hasCachedFloor_ = false; static constexpr float COLLISION_CACHE_DISTANCE = 0.15f; // Re-check every 15cm + + // Server-driven knockback state. + // When the server sends SMSG_MOVE_KNOCK_BACK, we apply horizontal + vertical + // impulse here and let the normal physics loop (gravity, collision) resolve it. + bool knockbackActive_ = false; + glm::vec2 knockbackHorizVel_ = glm::vec2(0.0f); // render-space horizontal velocity (units/s) + // Horizontal velocity decays via WoW-like drag so the player doesn't slide forever. + static constexpr float KNOCKBACK_HORIZ_DRAG = 4.5f; // exponential decay rate (1/s) }; } // namespace rendering diff --git a/src/core/application.cpp b/src/core/application.cpp index a2c80c91..065912dc 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -636,6 +636,11 @@ void Application::setState(AppState newState) { renderer->triggerMeleeSwing(); } }); + gameHandler->setKnockBackCallback([this](float vcos, float vsin, float hspeed, float vspeed) { + if (renderer && renderer->getCameraController()) { + renderer->getCameraController()->applyKnockBack(vcos, vsin, hspeed, vspeed); + } + }); } // Load quest marker models loadQuestMarkerModels(); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 91c7372c..76a4a053 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -11491,16 +11491,23 @@ void GameHandler::handleMoveKnockBack(network::Packet& packet) { ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 20) return; // counter(4) + vcos(4) + vsin(4) + hspeed(4) + vspeed(4) uint32_t counter = packet.readUInt32(); - [[maybe_unused]] float vcos = packet.readFloat(); - [[maybe_unused]] float vsin = packet.readFloat(); - [[maybe_unused]] float hspeed = packet.readFloat(); - [[maybe_unused]] float vspeed = packet.readFloat(); + float vcos = packet.readFloat(); + float vsin = packet.readFloat(); + float hspeed = packet.readFloat(); + float vspeed = packet.readFloat(); LOG_INFO("SMSG_MOVE_KNOCK_BACK: guid=0x", std::hex, guid, std::dec, - " counter=", counter, " hspeed=", hspeed, " vspeed=", vspeed); + " counter=", counter, " vcos=", vcos, " vsin=", vsin, + " hspeed=", hspeed, " vspeed=", vspeed); if (guid != playerGuid) return; + // Apply knockback physics locally so the player visually flies through the air. + // The callback forwards to CameraController::applyKnockBack(). + if (knockBackCallback_) { + knockBackCallback_(vcos, vsin, hspeed, vspeed); + } + if (!socket) return; uint16_t ackWire = wireOpcode(Opcode::CMSG_MOVE_KNOCK_BACK_ACK); if (ackWire == 0xFFFF) return; diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index cd6f7c27..bceb41ba 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -685,6 +685,20 @@ void CameraController::update(float deltaTime) { targetPos += movement * speed * physicsDeltaTime; } + // Apply server-driven knockback horizontal velocity (decays over time). + if (knockbackActive_) { + targetPos.x += knockbackHorizVel_.x * physicsDeltaTime; + targetPos.y += knockbackHorizVel_.y * physicsDeltaTime; + // Exponential drag: reduce each frame so the player decelerates naturally. + float drag = std::exp(-KNOCKBACK_HORIZ_DRAG * physicsDeltaTime); + knockbackHorizVel_ *= drag; + // Once negligible, clear the flag so collision/grounding work normally. + if (glm::length(knockbackHorizVel_) < 0.05f) { + knockbackActive_ = false; + knockbackHorizVel_ = glm::vec2(0.0f); + } + } + // Jump with input buffering and coyote time if (nowJump) jumpBufferTimer = JUMP_BUFFER_TIME; if (grounded) coyoteTimer = COYOTE_TIME; @@ -2096,5 +2110,21 @@ void CameraController::triggerMountJump() { } } +void CameraController::applyKnockBack(float vcos, float vsin, float hspeed, float vspeed) { + // The server sends (vcos, vsin) as the 2D direction vector in server/wire + // coordinate space. After the server→canonical→render swaps, the direction + // in render space is simply (vcos, vsin) — the two swaps cancel each other. + knockbackHorizVel_ = glm::vec2(vcos, vsin) * hspeed; + knockbackActive_ = true; + + // vspeed in the wire packet is negative when the server wants to launch the + // player upward (matches TrinityCore: data << float(-speedZ)). Negate it + // here to obtain the correct upward initial velocity. + verticalVelocity = -vspeed; + grounded = false; + coyoteTimer = 0.0f; + jumpBufferTimer = 0.0f; +} + } // namespace rendering } // namespace wowee