diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 56f4bae1..aba5a344 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -902,6 +902,15 @@ public: uint8_t getComboPoints() const { return comboPoints_; } uint64_t getComboTarget() const { return comboTarget_; } + // Death Knight rune state (6 runes: 0-1=Blood, 2-3=Unholy, 4-5=Frost; may become Death=3) + enum class RuneType : uint8_t { Blood = 0, Unholy = 1, Frost = 2, Death = 3 }; + struct RuneSlot { + RuneType type = RuneType::Blood; + bool ready = true; // Server-confirmed ready state + float readyFraction = 1.0f; // 0.0=depleted → 1.0=full (from server sync) + }; + const std::array& getPlayerRunes() const { return playerRunes_; } + struct FactionStandingInit { uint8_t flags = 0; int32_t standing = 0; @@ -2081,6 +2090,14 @@ private: float serverPitchRate_ = 3.14159f; bool playerDead_ = false; bool releasedSpirit_ = false; + // Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially + std::array playerRunes_ = [] { + std::array r{}; + r[0].type = r[1].type = RuneType::Blood; + r[2].type = r[3].type = RuneType::Unholy; + r[4].type = r[5].type = RuneType::Frost; + return r; + }(); uint64_t pendingSpiritHealerGuid_ = 0; bool resurrectPending_ = false; bool resurrectRequestPending_ = false; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index a197b9b5..cd944b47 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -267,6 +267,9 @@ private: bool spellIconDbLoaded_ = false; VkDescriptorSet getSpellIcon(uint32_t spellId, pipeline::AssetManager* am); + // Death Knight rune bar: client-predicted fill (0.0=depleted, 1.0=ready) for smooth animation + float runeClientFill_[6] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + // Action bar drag state (-1 = not dragging) int actionBarDragSlot_ = -1; VkDescriptorSet actionBarDragIcon_ = VK_NULL_HANDLE; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index d1570911..43a14f09 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1980,7 +1980,6 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_FORCE_DISPLAY_UPDATE: case Opcode::SMSG_FORCE_SEND_QUEUED_PACKETS: case Opcode::SMSG_FORCE_SET_VEHICLE_REC_ID: - case Opcode::SMSG_CONVERT_RUNE: case Opcode::SMSG_CORPSE_MAP_POSITION_QUERY_RESPONSE: case Opcode::SMSG_DAMAGE_CALC_LOG: case Opcode::SMSG_DYNAMIC_DROP_ROLL_RESULT: @@ -4457,11 +4456,49 @@ void GameHandler::handlePacket(network::Packet& packet) { packet.setReadPos(packet.getSize()); break; - // ---- DK rune tracking (not yet implemented) ---- - case Opcode::SMSG_ADD_RUNE_POWER: - case Opcode::SMSG_RESYNC_RUNES: - packet.setReadPos(packet.getSize()); + // ---- DK rune tracking ---- + case Opcode::SMSG_CONVERT_RUNE: { + // uint8 runeIndex + uint8 newRuneType (0=Blood,1=Unholy,2=Frost,3=Death) + if (packet.getSize() - packet.getReadPos() < 2) { + packet.setReadPos(packet.getSize()); + break; + } + uint8_t idx = packet.readUInt8(); + uint8_t type = packet.readUInt8(); + if (idx < 6) playerRunes_[idx].type = static_cast(type & 0x3); break; + } + case Opcode::SMSG_RESYNC_RUNES: { + // uint8 runeReadyMask (bit i=1 → rune i is ready) + // uint8[6] cooldowns (0=ready, 255=just used → readyFraction = 1 - val/255) + if (packet.getSize() - packet.getReadPos() < 7) { + packet.setReadPos(packet.getSize()); + break; + } + uint8_t readyMask = packet.readUInt8(); + for (int i = 0; i < 6; i++) { + uint8_t cd = packet.readUInt8(); + playerRunes_[i].ready = (readyMask & (1u << i)) != 0; + playerRunes_[i].readyFraction = 1.0f - cd / 255.0f; + if (playerRunes_[i].ready) playerRunes_[i].readyFraction = 1.0f; + } + break; + } + case Opcode::SMSG_ADD_RUNE_POWER: { + // uint32 runeMask (bit i=1 → rune i just became ready) + if (packet.getSize() - packet.getReadPos() < 4) { + packet.setReadPos(packet.getSize()); + break; + } + uint32_t runeMask = packet.readUInt32(); + for (int i = 0; i < 6; i++) { + if (runeMask & (1u << i)) { + playerRunes_[i].ready = true; + playerRunes_[i].readyFraction = 1.0f; + } + } + break; + } // ---- Spell combat logs (consume) ---- case Opcode::SMSG_AURACASTLOG: diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 06e9bbe6..fd637c40 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1815,6 +1815,56 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); } } + + // Death Knight rune bar (class 6) — 6 colored squares with fill fraction + if (gameHandler.getPlayerClass() == 6) { + const auto& runes = gameHandler.getPlayerRunes(); + float dt = ImGui::GetIO().DeltaTime; + + ImGui::Spacing(); + ImVec2 cursor = ImGui::GetCursorScreenPos(); + float totalW = ImGui::GetContentRegionAvail().x; + float spacing = 3.0f; + float squareW = (totalW - spacing * 5.0f) / 6.0f; + float squareH = 14.0f; + ImDrawList* dl = ImGui::GetWindowDrawList(); + + for (int i = 0; i < 6; i++) { + // Client-side prediction: advance fill over ~10s cooldown + runeClientFill_[i] = runes[i].ready ? 1.0f + : std::min(runeClientFill_[i] + dt / 10.0f, runes[i].readyFraction + 0.02f); + runeClientFill_[i] = std::clamp(runeClientFill_[i], 0.0f, runes[i].ready ? 1.0f : 0.97f); + + float x0 = cursor.x + i * (squareW + spacing); + float y0 = cursor.y; + float x1 = x0 + squareW; + float y1 = y0 + squareH; + + // Background (dark) + dl->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y1), + IM_COL32(30, 30, 30, 200), 2.0f); + + // Fill color by rune type + ImVec4 fc; + switch (runes[i].type) { + case game::GameHandler::RuneType::Blood: fc = ImVec4(0.85f, 0.12f, 0.12f, 1.0f); break; + case game::GameHandler::RuneType::Unholy: fc = ImVec4(0.20f, 0.72f, 0.20f, 1.0f); break; + case game::GameHandler::RuneType::Frost: fc = ImVec4(0.30f, 0.55f, 0.90f, 1.0f); break; + case game::GameHandler::RuneType::Death: fc = ImVec4(0.55f, 0.20f, 0.70f, 1.0f); break; + default: fc = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); break; + } + float fillX = x0 + (x1 - x0) * runeClientFill_[i]; + dl->AddRectFilled(ImVec2(x0, y0), ImVec2(fillX, y1), + ImGui::ColorConvertFloat4ToU32(fc), 2.0f); + + // Border + ImU32 borderCol = runes[i].ready + ? IM_COL32(220, 220, 220, 180) + : IM_COL32(100, 100, 100, 160); + dl->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), borderCol, 2.0f); + } + ImGui::Dummy(ImVec2(totalW, squareH)); + } } ImGui::End();