Implement Death Knight rune tracking and rune bar UI

Parse SMSG_RESYNC_RUNES, SMSG_ADD_RUNE_POWER, and SMSG_CONVERT_RUNE to
track the state of all 6 DK runes (Blood/Unholy/Frost/Death type,
ready flag, and cooldown fraction). Render a six-square rune bar below
the Runic Power bar when the player is class 6, with per-type colors
(Blood=red, Unholy=green, Frost=blue, Death=purple) and client-side
fill animation so runes visibly refill over the 10s cooldown.
This commit is contained in:
Kelsi 2026-03-09 18:28:03 -07:00
parent 819a38a7ca
commit c887a460ea
4 changed files with 112 additions and 5 deletions

View file

@ -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<RuneSlot, 6>& 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<RuneSlot, 6> playerRunes_ = [] {
std::array<RuneSlot, 6> 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;

View file

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

View file

@ -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<RuneType>(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:

View file

@ -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();