feat: show stat gains in level-up toast from SMSG_LEVELUP_INFO

Parse hp/mana/str/agi/sta/int/spi deltas from SMSG_LEVELUP_INFO payload
and display them in green below the "You have reached level X!" banner.
Extends DING_DURATION to 4s to give players time to read the gains.
This commit is contained in:
Kelsi 2026-03-12 17:54:49 -07:00
parent 6df8c72cf7
commit 6957ba97ea
4 changed files with 80 additions and 9 deletions

View file

@ -1438,6 +1438,14 @@ public:
using LevelUpCallback = std::function<void(uint32_t newLevel)>;
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(uint64_t guid, uint32_t newLevel)>;
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_;

View file

@ -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 = {});
};

View file

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

View file

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