From 9aa4b223dcca7779f89ef25f2505c5374175138e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 19:37:53 -0700 Subject: [PATCH] feat: implement SMSG_CAMERA_SHAKE with sinusoidal camera shake effect - Add triggerShake(magnitude, frequency, duration) to CameraController - Apply envelope-decaying sinusoidal XYZ offset to camera in update() - Handle SMSG_CAMERA_SHAKE opcode in GameHandler dispatch - Translate shakeId to magnitude (minor <50: 0.04, larger: 0.08 world units) - Wire CameraShakeCallback from GameHandler through to CameraController - Shake uses 18Hz oscillation with 30% fade-out envelope at end of duration --- include/game/game_handler.hpp | 6 ++++++ include/rendering/camera_controller.hpp | 12 +++++++++++ src/core/application.cpp | 5 +++++ src/game/game_handler.cpp | 19 +++++++++++++++++ src/rendering/camera_controller.cpp | 28 +++++++++++++++++++++++++ 5 files changed, 70 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 87f35809..2fe0dcec 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -882,6 +882,11 @@ public: using KnockBackCallback = std::function; void setKnockBackCallback(KnockBackCallback cb) { knockBackCallback_ = std::move(cb); } + // Camera shake callback: called when server sends SMSG_CAMERA_SHAKE. + // Parameters: magnitude (world units), frequency (Hz), duration (seconds). + using CameraShakeCallback = std::function; + void setCameraShakeCallback(CameraShakeCallback cb) { cameraShakeCallback_ = std::move(cb); } + // Unstuck callback (resets player Z to floor height) using UnstuckCallback = std::function; void setUnstuckCallback(UnstuckCallback cb) { unstuckCallback_ = std::move(cb); } @@ -2323,6 +2328,7 @@ private: // ---- Phase 3: Spells ---- WorldEntryCallback worldEntryCallback_; KnockBackCallback knockBackCallback_; + CameraShakeCallback cameraShakeCallback_; UnstuckCallback unstuckCallback_; UnstuckCallback unstuckGyCallback_; UnstuckCallback unstuckHearthCallback_; diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 7401ffdd..fbddd523 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -129,6 +129,12 @@ public: // vspeed: raw packet vspeed field (server sends negative for upward launch) void applyKnockBack(float vcos, float vsin, float hspeed, float vspeed); + // Trigger a camera shake effect (e.g. from SMSG_CAMERA_SHAKE). + // magnitude: peak positional offset in world units + // frequency: oscillation frequency in Hz + // duration: shake duration in seconds + void triggerShake(float magnitude, float frequency, float duration); + // For first-person player hiding void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) { characterRenderer = cr; @@ -369,6 +375,12 @@ private: 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) + + // Camera shake state (SMSG_CAMERA_SHAKE) + float shakeElapsed_ = 0.0f; + float shakeDuration_ = 0.0f; + float shakeMagnitude_ = 0.0f; + float shakeFrequency_ = 0.0f; }; } // namespace rendering diff --git a/src/core/application.cpp b/src/core/application.cpp index 9ad75cc6..396c260f 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -646,6 +646,11 @@ void Application::setState(AppState newState) { renderer->getCameraController()->applyKnockBack(vcos, vsin, hspeed, vspeed); } }); + gameHandler->setCameraShakeCallback([this](float magnitude, float frequency, float duration) { + if (renderer && renderer->getCameraController()) { + renderer->getCameraController()->triggerShake(magnitude, frequency, duration); + } + }); } // Load quest marker models loadQuestMarkerModels(); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 3f8183ad..903c5799 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2760,6 +2760,25 @@ void GameHandler::handlePacket(network::Packet& packet) { handleMoveKnockBack(packet); break; + case Opcode::SMSG_CAMERA_SHAKE: { + // uint32 shakeID (CameraShakes.dbc), uint32 shakeType + // We don't parse CameraShakes.dbc; apply a hardcoded moderate shake. + if (packet.getSize() - packet.getReadPos() >= 8) { + uint32_t shakeId = packet.readUInt32(); + uint32_t shakeType = packet.readUInt32(); + (void)shakeType; + // Map shakeId ranges to approximate magnitudes: + // IDs < 50: minor environmental (0.04), others: larger boss effects (0.08) + float magnitude = (shakeId < 50) ? 0.04f : 0.08f; + if (cameraShakeCallback_) { + cameraShakeCallback_(magnitude, 18.0f, 0.5f); + } + LOG_DEBUG("SMSG_CAMERA_SHAKE: id=", shakeId, " type=", shakeType, + " magnitude=", magnitude); + } + break; + } + case Opcode::SMSG_CLIENT_CONTROL_UPDATE: { // Minimal parse: PackedGuid + uint8 allowMovement. if (packet.getSize() - packet.getReadPos() < 2) { diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 50872d46..a34f05f1 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -140,6 +140,17 @@ std::optional CameraController::getCachedFloorHeight(float x, float y, fl return result; } +void CameraController::triggerShake(float magnitude, float frequency, float duration) { + // Allow stronger shake to override weaker; don't allow zero magnitude. + if (magnitude <= 0.0f || duration <= 0.0f) return; + if (magnitude > shakeMagnitude_ || shakeElapsed_ >= shakeDuration_) { + shakeMagnitude_ = magnitude; + shakeFrequency_ = frequency; + shakeDuration_ = duration; + shakeElapsed_ = 0.0f; + } +} + void CameraController::update(float deltaTime) { if (!enabled || !camera) { return; @@ -1859,6 +1870,23 @@ void CameraController::update(float deltaTime) { wasFalling = !grounded && verticalVelocity <= 0.0f; // R key is now handled above with chat safeguard (WantTextInput check) + + // Camera shake (SMSG_CAMERA_SHAKE): apply sinusoidal offset to final camera position. + if (shakeElapsed_ < shakeDuration_) { + shakeElapsed_ += deltaTime; + float t = shakeElapsed_ / shakeDuration_; + // Envelope: fade out over the last 30% of shake duration + float envelope = (t < 0.7f) ? 1.0f : (1.0f - (t - 0.7f) / 0.3f); + float theta = shakeElapsed_ * shakeFrequency_ * 2.0f * 3.14159265f; + glm::vec3 offset( + shakeMagnitude_ * envelope * std::sin(theta), + shakeMagnitude_ * envelope * std::cos(theta * 1.3f), + shakeMagnitude_ * envelope * std::sin(theta * 0.7f) * 0.5f + ); + if (camera) { + camera->setPosition(camera->getPosition() + offset); + } + } } void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {