diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 037a0ea4..db702467 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1438,6 +1438,14 @@ public: using LevelUpCallback = std::function; void setLevelUpCallback(LevelUpCallback cb) { levelUpCallback_ = std::move(cb); } + // Stat deltas from the last SMSG_LEVELUP_INFO (valid until next level-up) + struct LevelUpDeltas { + uint32_t hp = 0; + uint32_t mana = 0; + uint32_t str = 0, agi = 0, sta = 0, intel = 0, spi = 0; + }; + const LevelUpDeltas& getLastLevelUpDeltas() const { return lastLevelUpDeltas_; } + // Other player level-up callback — fires when another player gains a level using OtherPlayerLevelUpCallback = std::function; void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); } @@ -2793,6 +2801,7 @@ private: NpcVendorCallback npcVendorCallback_; ChargeCallback chargeCallback_; LevelUpCallback levelUpCallback_; + LevelUpDeltas lastLevelUpDeltas_; OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_; AchievementEarnedCallback achievementEarnedCallback_; AreaDiscoveryCallback areaDiscoveryCallback_; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index bbb5ffa8..09f80551 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -511,9 +511,12 @@ private: bool leftClickWasPress_ = false; // Level-up ding animation - static constexpr float DING_DURATION = 3.0f; + static constexpr float DING_DURATION = 4.0f; float dingTimer_ = 0.0f; uint32_t dingLevel_ = 0; + uint32_t dingHpDelta_ = 0; + uint32_t dingManaDelta_ = 0; + uint32_t dingStats_[5] = {}; // str/agi/sta/int/spi deltas void renderDingEffect(); // Achievement toast banner @@ -616,7 +619,9 @@ private: size_t dpsLogSeenCount_ = 0; // log entries already scanned public: - void triggerDing(uint32_t newLevel); + void triggerDing(uint32_t newLevel, uint32_t hpDelta = 0, uint32_t manaDelta = 0, + uint32_t str = 0, uint32_t agi = 0, uint32_t sta = 0, + uint32_t intel = 0, uint32_t spi = 0); void triggerAchievementToast(uint32_t achievementId, std::string name = {}); }; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 485d96aa..6bc076da 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3823,10 +3823,21 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_LEVELUP_INFO: case Opcode::SMSG_LEVELUP_INFO_ALT: { // Server-authoritative level-up event. - // First field is always the new level in Classic/TBC/WotLK-era layouts. + // WotLK layout: uint32 newLevel + uint32 hpDelta + uint32 manaDelta + 5x uint32 statDeltas if (packet.getSize() - packet.getReadPos() >= 4) { uint32_t newLevel = packet.readUInt32(); if (newLevel > 0) { + // Parse stat deltas (WotLK layout has 7 more uint32s) + lastLevelUpDeltas_ = {}; + if (packet.getSize() - packet.getReadPos() >= 28) { + lastLevelUpDeltas_.hp = packet.readUInt32(); + lastLevelUpDeltas_.mana = packet.readUInt32(); + lastLevelUpDeltas_.str = packet.readUInt32(); + lastLevelUpDeltas_.agi = packet.readUInt32(); + lastLevelUpDeltas_.sta = packet.readUInt32(); + lastLevelUpDeltas_.intel = packet.readUInt32(); + lastLevelUpDeltas_.spi = packet.readUInt32(); + } uint32_t oldLevel = serverPlayerLevel_; serverPlayerLevel_ = std::max(serverPlayerLevel_, newLevel); for (auto& ch : characters) { @@ -3840,7 +3851,6 @@ void GameHandler::handlePacket(network::Packet& packet) { } } } - // Remaining payload (hp/mana/stat deltas) is optional for our client. packet.setReadPos(packet.getSize()); break; } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 8be6697a..7429f04b 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -284,10 +284,11 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Set up level-up callback (once) if (!levelUpCallbackSet_) { - gameHandler.setLevelUpCallback([this](uint32_t newLevel) { + gameHandler.setLevelUpCallback([this, &gameHandler](uint32_t newLevel) { levelUpFlashAlpha_ = 1.0f; levelUpDisplayLevel_ = newLevel; - triggerDing(newLevel); + const auto& d = gameHandler.getLastLevelUpDeltas(); + triggerDing(newLevel, d.hp, d.mana, d.str, d.agi, d.sta, d.intel, d.spi); }); levelUpCallbackSet_ = true; } @@ -18058,9 +18059,18 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { // Level-Up Ding Animation // ============================================================ -void GameScreen::triggerDing(uint32_t newLevel) { - dingTimer_ = DING_DURATION; - dingLevel_ = newLevel; +void GameScreen::triggerDing(uint32_t newLevel, uint32_t hpDelta, uint32_t manaDelta, + uint32_t str, uint32_t agi, uint32_t sta, + uint32_t intel, uint32_t spi) { + dingTimer_ = DING_DURATION; + dingLevel_ = newLevel; + dingHpDelta_ = hpDelta; + dingManaDelta_ = manaDelta; + dingStats_[0] = str; + dingStats_[1] = agi; + dingStats_[2] = sta; + dingStats_[3] = intel; + dingStats_[4] = spi; auto* renderer = core::Application::getInstance().getRenderer(); if (renderer) { @@ -18106,6 +18116,43 @@ void GameScreen::renderDingEffect() { // Gold text draw->AddText(font, fontSize, ImVec2(tx, ty), IM_COL32(255, 210, 0, (int)(alpha * 255)), buf); + + // Stat gains below the main text (shown only if server sent deltas) + bool hasStatGains = (dingHpDelta_ > 0 || dingManaDelta_ > 0 || + dingStats_[0] || dingStats_[1] || dingStats_[2] || + dingStats_[3] || dingStats_[4]); + if (hasStatGains) { + float smallSize = baseSize * 0.95f; + float yOff = ty + sz.y + 6.0f; + + // Build stat delta string: "+150 HP +80 Mana +2 Str +2 Agi ..." + static const char* kStatLabels[] = { "Str", "Agi", "Sta", "Int", "Spi" }; + char statBuf[128]; + int written = 0; + if (dingHpDelta_ > 0) + written += snprintf(statBuf + written, sizeof(statBuf) - written, + "+%u HP ", dingHpDelta_); + if (dingManaDelta_ > 0) + written += snprintf(statBuf + written, sizeof(statBuf) - written, + "+%u Mana ", dingManaDelta_); + for (int i = 0; i < 5 && written < (int)sizeof(statBuf) - 1; ++i) { + if (dingStats_[i] > 0) + written += snprintf(statBuf + written, sizeof(statBuf) - written, + "+%u %s ", dingStats_[i], kStatLabels[i]); + } + // Trim trailing spaces + while (written > 0 && statBuf[written - 1] == ' ') --written; + statBuf[written] = '\0'; + + if (written > 0) { + ImVec2 ssz = font->CalcTextSizeA(smallSize, FLT_MAX, 0.0f, statBuf); + float stx = cx - ssz.x * 0.5f; + draw->AddText(font, smallSize, ImVec2(stx + 1, yOff + 1), + IM_COL32(0, 0, 0, (int)(alpha * 160)), statBuf); + draw->AddText(font, smallSize, ImVec2(stx, yOff), + IM_COL32(100, 220, 100, (int)(alpha * 230)), statBuf); + } + } } void GameScreen::triggerAchievementToast(uint32_t achievementId, std::string name) {