physics: implement knockback simulation from SMSG_MOVE_KNOCK_BACK

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.
This commit is contained in:
Kelsi 2026-03-10 12:28:11 -07:00
parent dd3f9e5b9e
commit c622fde7be
5 changed files with 67 additions and 5 deletions

View file

@ -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();

View file

@ -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;

View file

@ -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