game,ui: add rest state tracking and rested XP bar overlay

- Track PLAYER_REST_STATE_EXPERIENCE update field for all expansions
  (WotLK=636, Classic=718, TBC=928, Turtle=718)
- Set isResting_ flag from SMSG_SET_REST_START packet
- XP bar shows rested bonus as a lighter purple overlay extending
  beyond the current fill to (currentXp + restedXp) position
- Tooltip text changes to "%u / %u XP  (+%u rested)" when bonus exists
- "zzz" indicator shown at bar right edge while resting
This commit is contained in:
Kelsi 2026-03-10 07:35:30 -07:00
parent 0ea8e55ad4
commit 2a9d26e1ea
8 changed files with 49 additions and 8 deletions

View file

@ -22,6 +22,7 @@
"PLAYER_BYTES_2": 192, "PLAYER_BYTES_2": 192,
"PLAYER_XP": 716, "PLAYER_XP": 716,
"PLAYER_NEXT_LEVEL_XP": 717, "PLAYER_NEXT_LEVEL_XP": 717,
"PLAYER_REST_STATE_EXPERIENCE": 718,
"PLAYER_FIELD_COINAGE": 1176, "PLAYER_FIELD_COINAGE": 1176,
"PLAYER_QUEST_LOG_START": 198, "PLAYER_QUEST_LOG_START": 198,
"PLAYER_FIELD_INV_SLOT_HEAD": 486, "PLAYER_FIELD_INV_SLOT_HEAD": 486,

View file

@ -22,6 +22,7 @@
"PLAYER_BYTES_2": 238, "PLAYER_BYTES_2": 238,
"PLAYER_XP": 926, "PLAYER_XP": 926,
"PLAYER_NEXT_LEVEL_XP": 927, "PLAYER_NEXT_LEVEL_XP": 927,
"PLAYER_REST_STATE_EXPERIENCE": 928,
"PLAYER_FIELD_COINAGE": 1441, "PLAYER_FIELD_COINAGE": 1441,
"PLAYER_QUEST_LOG_START": 244, "PLAYER_QUEST_LOG_START": 244,
"PLAYER_FIELD_INV_SLOT_HEAD": 650, "PLAYER_FIELD_INV_SLOT_HEAD": 650,

View file

@ -22,6 +22,7 @@
"PLAYER_BYTES_2": 192, "PLAYER_BYTES_2": 192,
"PLAYER_XP": 716, "PLAYER_XP": 716,
"PLAYER_NEXT_LEVEL_XP": 717, "PLAYER_NEXT_LEVEL_XP": 717,
"PLAYER_REST_STATE_EXPERIENCE": 718,
"PLAYER_FIELD_COINAGE": 1176, "PLAYER_FIELD_COINAGE": 1176,
"PLAYER_QUEST_LOG_START": 198, "PLAYER_QUEST_LOG_START": 198,
"PLAYER_FIELD_INV_SLOT_HEAD": 486, "PLAYER_FIELD_INV_SLOT_HEAD": 486,
@ -35,4 +36,4 @@
"ITEM_FIELD_STACK_COUNT": 14, "ITEM_FIELD_STACK_COUNT": 14,
"CONTAINER_FIELD_NUM_SLOTS": 48, "CONTAINER_FIELD_NUM_SLOTS": 48,
"CONTAINER_FIELD_SLOT_1": 50 "CONTAINER_FIELD_SLOT_1": 50
} }

View file

@ -22,6 +22,7 @@
"PLAYER_BYTES_2": 154, "PLAYER_BYTES_2": 154,
"PLAYER_XP": 634, "PLAYER_XP": 634,
"PLAYER_NEXT_LEVEL_XP": 635, "PLAYER_NEXT_LEVEL_XP": 635,
"PLAYER_REST_STATE_EXPERIENCE": 636,
"PLAYER_FIELD_COINAGE": 1170, "PLAYER_FIELD_COINAGE": 1170,
"PLAYER_QUEST_LOG_START": 158, "PLAYER_QUEST_LOG_START": 158,
"PLAYER_FIELD_INV_SLOT_HEAD": 324, "PLAYER_FIELD_INV_SLOT_HEAD": 324,

View file

@ -640,6 +640,8 @@ public:
// XP tracking // XP tracking
uint32_t getPlayerXp() const { return playerXp_; } uint32_t getPlayerXp() const { return playerXp_; }
uint32_t getPlayerNextLevelXp() const { return playerNextLevelXp_; } uint32_t getPlayerNextLevelXp() const { return playerNextLevelXp_; }
uint32_t getPlayerRestedXp() const { return playerRestedXp_; }
bool isPlayerResting() const { return isResting_; }
uint32_t getPlayerLevel() const { return serverPlayerLevel_; } uint32_t getPlayerLevel() const { return serverPlayerLevel_; }
const std::vector<uint32_t>& getPlayerExploredZoneMasks() const { return playerExploredZones_; } const std::vector<uint32_t>& getPlayerExploredZoneMasks() const { return playerExploredZones_; }
bool hasPlayerExploredZoneMasks() const { return hasPlayerExploredZones_; } bool hasPlayerExploredZoneMasks() const { return hasPlayerExploredZones_; }
@ -2199,6 +2201,8 @@ private:
// ---- XP tracking ---- // ---- XP tracking ----
uint32_t playerXp_ = 0; uint32_t playerXp_ = 0;
uint32_t playerNextLevelXp_ = 0; uint32_t playerNextLevelXp_ = 0;
uint32_t playerRestedXp_ = 0;
bool isResting_ = false;
uint32_t serverPlayerLevel_ = 1; uint32_t serverPlayerLevel_ = 1;
static uint32_t xpForLevel(uint32_t level); static uint32_t xpForLevel(uint32_t level);

View file

@ -41,6 +41,7 @@ enum class UF : uint16_t {
PLAYER_BYTES_2, PLAYER_BYTES_2,
PLAYER_XP, PLAYER_XP,
PLAYER_NEXT_LEVEL_XP, PLAYER_NEXT_LEVEL_XP,
PLAYER_REST_STATE_EXPERIENCE,
PLAYER_FIELD_COINAGE, PLAYER_FIELD_COINAGE,
PLAYER_QUEST_LOG_START, PLAYER_QUEST_LOG_START,
PLAYER_FIELD_INV_SLOT_HEAD, PLAYER_FIELD_INV_SLOT_HEAD,

View file

@ -4644,8 +4644,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::SMSG_SET_REST_START: { case Opcode::SMSG_SET_REST_START: {
if (packet.getSize() - packet.getReadPos() >= 4) { if (packet.getSize() - packet.getReadPos() >= 4) {
uint32_t restTrigger = packet.readUInt32(); uint32_t restTrigger = packet.readUInt32();
addSystemChatMessage(restTrigger > 0 ? "You are now resting." isResting_ = (restTrigger > 0);
: "You are no longer resting."); addSystemChatMessage(isResting_ ? "You are now resting."
: "You are no longer resting.");
} }
break; break;
} }
@ -7835,6 +7836,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
bool slotsChanged = false; bool slotsChanged = false;
const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP); const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP);
const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP); const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP);
const uint16_t ufPlayerRestedXp = fieldIndex(UF::PLAYER_REST_STATE_EXPERIENCE);
const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE); const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES); const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES);
@ -7842,6 +7844,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
for (const auto& [key, val] : block.fields) { for (const auto& [key, val] : block.fields) {
if (key == ufPlayerXp) { playerXp_ = val; } if (key == ufPlayerXp) { playerXp_ = val; }
else if (key == ufPlayerNextXp) { playerNextLevelXp_ = val; } else if (key == ufPlayerNextXp) { playerNextLevelXp_ = val; }
else if (ufPlayerRestedXp != 0xFFFF && key == ufPlayerRestedXp) { playerRestedXp_ = val; }
else if (key == ufPlayerLevel) { else if (key == ufPlayerLevel) {
serverPlayerLevel_ = val; serverPlayerLevel_ = val;
for (auto& ch : characters) { for (auto& ch : characters) {

View file

@ -4409,7 +4409,9 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) {
uint32_t nextLevelXp = gameHandler.getPlayerNextLevelXp(); uint32_t nextLevelXp = gameHandler.getPlayerNextLevelXp();
if (nextLevelXp == 0) return; // No XP data yet (level 80 or not initialized) if (nextLevelXp == 0) return; // No XP data yet (level 80 or not initialized)
uint32_t currentXp = gameHandler.getPlayerXp(); uint32_t currentXp = gameHandler.getPlayerXp();
uint32_t restedXp = gameHandler.getPlayerRestedXp();
bool isResting = gameHandler.isPlayerResting();
auto* window = core::Application::getInstance().getWindow(); auto* window = core::Application::getInstance().getWindow();
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f; float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f; float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
@ -4449,9 +4451,10 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) {
ImVec2 barMax = ImVec2(barMin.x + barSize.x, barMin.y + barSize.y); ImVec2 barMax = ImVec2(barMin.x + barSize.x, barMin.y + barSize.y);
auto* drawList = ImGui::GetWindowDrawList(); auto* drawList = ImGui::GetWindowDrawList();
ImU32 bg = IM_COL32(15, 15, 20, 220); ImU32 bg = IM_COL32(15, 15, 20, 220);
ImU32 fg = IM_COL32(148, 51, 238, 255); ImU32 fg = IM_COL32(148, 51, 238, 255);
ImU32 seg = IM_COL32(35, 35, 45, 255); ImU32 fgRest = IM_COL32(200, 170, 255, 220); // lighter purple for rested portion
ImU32 seg = IM_COL32(35, 35, 45, 255);
drawList->AddRectFilled(barMin, barMax, bg, 2.0f); drawList->AddRectFilled(barMin, barMax, bg, 2.0f);
drawList->AddRect(barMin, barMax, IM_COL32(80, 80, 90, 220), 2.0f); drawList->AddRect(barMin, barMax, IM_COL32(80, 80, 90, 220), 2.0f);
@ -4460,6 +4463,19 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) {
drawList->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y), fg, 2.0f); drawList->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y), fg, 2.0f);
} }
// Rested XP overlay: draw from current XP fill to (currentXp + restedXp) fill
if (restedXp > 0) {
float restedEndPct = std::min(1.0f, static_cast<float>(currentXp + restedXp)
/ static_cast<float>(nextLevelXp));
float restedStartX = barMin.x + fillW;
float restedEndX = barMin.x + barSize.x * restedEndPct;
if (restedEndX > restedStartX) {
drawList->AddRectFilled(ImVec2(restedStartX, barMin.y),
ImVec2(restedEndX, barMax.y),
fgRest, 2.0f);
}
}
const int segments = 20; const int segments = 20;
float segW = barSize.x / static_cast<float>(segments); float segW = barSize.x / static_cast<float>(segments);
for (int i = 1; i < segments; ++i) { for (int i = 1; i < segments; ++i) {
@ -4467,8 +4483,21 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) {
drawList->AddLine(ImVec2(x, barMin.y + 1.0f), ImVec2(x, barMax.y - 1.0f), seg, 1.0f); drawList->AddLine(ImVec2(x, barMin.y + 1.0f), ImVec2(x, barMax.y - 1.0f), seg, 1.0f);
} }
// Rest indicator "zzz" to the right of the bar when resting
if (isResting) {
const char* zzz = "zzz";
ImVec2 zSize = ImGui::CalcTextSize(zzz);
float zx = barMax.x - zSize.x - 4.0f;
float zy = barMin.y + (barSize.y - zSize.y) * 0.5f;
drawList->AddText(ImVec2(zx, zy), IM_COL32(180, 150, 255, 220), zzz);
}
char overlay[96]; char overlay[96];
snprintf(overlay, sizeof(overlay), "%u / %u XP", currentXp, nextLevelXp); if (restedXp > 0) {
snprintf(overlay, sizeof(overlay), "%u / %u XP (+%u rested)", currentXp, nextLevelXp, restedXp);
} else {
snprintf(overlay, sizeof(overlay), "%u / %u XP", currentXp, nextLevelXp);
}
ImVec2 textSize = ImGui::CalcTextSize(overlay); ImVec2 textSize = ImGui::CalcTextSize(overlay);
float tx = barMin.x + (barSize.x - textSize.x) * 0.5f; float tx = barMin.x + (barSize.x - textSize.x) * 0.5f;
float ty = barMin.y + (barSize.y - textSize.y) * 0.5f; float ty = barMin.y + (barSize.y - textSize.y) * 0.5f;