diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 70dd0eec..495e31ed 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -514,6 +514,21 @@ public: const std::vector& getCombatText() const { return combatText; } void updateCombatText(float deltaTime); + // Threat + struct ThreatEntry { + uint64_t victimGuid = 0; + uint32_t threat = 0; + }; + // Returns the current threat list for a given unit GUID (from last SMSG_THREAT_UPDATE) + const std::vector* getThreatList(uint64_t unitGuid) const { + auto it = threatLists_.find(unitGuid); + return (it != threatLists_.end()) ? &it->second : nullptr; + } + // Returns the threat list for the player's current target, or nullptr + const std::vector* getTargetThreatList() const { + return targetGuid ? getThreatList(targetGuid) : nullptr; + } + // ---- Phase 3: Spells ---- void castSpell(uint32_t spellId, uint64_t targetGuid = 0); void cancelCast(); @@ -2047,6 +2062,8 @@ private: float autoAttackFacingSyncTimer_ = 0.0f; // Periodic facing sync while meleeing std::unordered_set hostileAttackers_; std::vector combatText; + // unitGuid → sorted threat list (descending by threat value) + std::unordered_map> threatLists_; // ---- Phase 3: Spells ---- WorldEntryCallback worldEntryCallback_; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 1c5105ad..2f3ff0aa 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -368,6 +368,10 @@ private: // Inspect window bool showInspectWindow_ = false; void renderInspectWindow(game::GameHandler& gameHandler); + + // Threat window + bool showThreatWindow_ = false; + void renderThreatWindow(game::GameHandler& gameHandler); uint8_t lfgRoles_ = 0x08; // default: DPS (0x02=tank, 0x04=healer, 0x08=dps) uint32_t lfgSelectedDungeon_ = 861; // default: random dungeon (entry 861 = Random Dungeon WotLK) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 25935eff..f7d8350c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2331,24 +2331,51 @@ void GameHandler::handlePacket(network::Packet& packet) { break; case Opcode::SMSG_THREAT_CLEAR: // All threat dropped on the local player (e.g. Vanish, Feign Death) - // No local state to clear — informational + threatLists_.clear(); LOG_DEBUG("SMSG_THREAT_CLEAR: threat wiped"); break; case Opcode::SMSG_THREAT_REMOVE: { // packed_guid (unit) + packed_guid (victim whose threat was removed) - if (packet.getSize() - packet.getReadPos() >= 1) { - (void)UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() >= 1) { - (void)UpdateObjectParser::readPackedGuid(packet); - } + if (packet.getSize() - packet.getReadPos() < 1) break; + uint64_t unitGuid = UpdateObjectParser::readPackedGuid(packet); + if (packet.getSize() - packet.getReadPos() < 1) break; + uint64_t victimGuid = UpdateObjectParser::readPackedGuid(packet); + auto it = threatLists_.find(unitGuid); + if (it != threatLists_.end()) { + auto& list = it->second; + list.erase(std::remove_if(list.begin(), list.end(), + [victimGuid](const ThreatEntry& e){ return e.victimGuid == victimGuid; }), + list.end()); + if (list.empty()) threatLists_.erase(it); } break; } - case Opcode::SMSG_HIGHEST_THREAT_UPDATE: { - // packed_guid (tank) + packed_guid (new highest threat unit) + uint32 count - // + count × (packed_guid victim + uint32 threat) - // Informational — no threat UI yet; consume to suppress warnings - packet.setReadPos(packet.getSize()); + case Opcode::SMSG_HIGHEST_THREAT_UPDATE: + case Opcode::SMSG_THREAT_UPDATE: { + // Both packets share the same format: + // packed_guid (unit) + packed_guid (highest-threat target or target, unused here) + // + uint32 count + count × (packed_guid victim + uint32 threat) + if (packet.getSize() - packet.getReadPos() < 1) break; + uint64_t unitGuid = UpdateObjectParser::readPackedGuid(packet); + if (packet.getSize() - packet.getReadPos() < 1) break; + (void)UpdateObjectParser::readPackedGuid(packet); // highest-threat / current target + if (packet.getSize() - packet.getReadPos() < 4) break; + uint32_t cnt = packet.readUInt32(); + if (cnt > 100) { packet.setReadPos(packet.getSize()); break; } // sanity + std::vector list; + list.reserve(cnt); + for (uint32_t i = 0; i < cnt; ++i) { + if (packet.getSize() - packet.getReadPos() < 1) break; + ThreatEntry entry; + entry.victimGuid = UpdateObjectParser::readPackedGuid(packet); + if (packet.getSize() - packet.getReadPos() < 4) break; + entry.threat = packet.readUInt32(); + list.push_back(entry); + } + // Sort descending by threat so highest is first + std::sort(list.begin(), list.end(), + [](const ThreatEntry& a, const ThreatEntry& b){ return a.threat > b.threat; }); + threatLists_[unitGuid] = std::move(list); break; } @@ -5656,22 +5683,6 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } - case Opcode::SMSG_THREAT_UPDATE: { - // packed_guid (unit) + packed_guid (target) + uint32 count - // + count × (packed_guid victim + uint32 threat) — consume to suppress warnings - if (packet.getSize() - packet.getReadPos() < 1) break; - (void)UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 1) break; - (void)UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t cnt = packet.readUInt32(); - for (uint32_t i = 0; i < cnt && packet.getSize() - packet.getReadPos() >= 1; ++i) { - (void)UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() >= 4) - packet.readUInt32(); - } - break; - } case Opcode::SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT: { // uint32 slot + packed_guid unit (0 packed = clear slot) if (packet.getSize() - packet.getReadPos() < 5) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 54b4c95c..40d330e6 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -500,6 +500,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderAchievementWindow(gameHandler); renderGmTicketWindow(gameHandler); renderInspectWindow(gameHandler); + renderThreatWindow(gameHandler); // renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now if (showMinimap_) { renderMinimapMarkers(gameHandler); @@ -2728,6 +2729,15 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { float distance = std::sqrt(dx*dx + dy*dy + dz*dz); ImGui::TextDisabled("%.1f yd", distance); + // Threat button (shown when in combat and threat data is available) + if (gameHandler.getTargetThreatList()) { + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.1f, 0.1f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 0.9f)); + if (ImGui::SmallButton("Threat")) showThreatWindow_ = !showThreatWindow_; + ImGui::PopStyleColor(2); + } + // Target auras (buffs/debuffs) const auto& targetAuras = gameHandler.getTargetAuras(); int activeAuras = 0; @@ -3152,6 +3162,13 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /threat command + if (cmdLower == "threat") { + showThreatWindow_ = !showThreatWindow_; + chatInputBuffer[0] = '\0'; + return; + } + // /time command if (cmdLower == "time") { gameHandler.queryServerTime(); @@ -14490,6 +14507,79 @@ void GameScreen::renderGmTicketWindow(game::GameHandler& gameHandler) { ImGui::End(); } +// ─── Threat Window ──────────────────────────────────────────────────────────── +void GameScreen::renderThreatWindow(game::GameHandler& gameHandler) { + if (!showThreatWindow_) return; + + const auto* list = gameHandler.getTargetThreatList(); + + ImGui::SetNextWindowSize(ImVec2(280, 220), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(10, 300), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowBgAlpha(0.85f); + + if (!ImGui::Begin("Threat###ThreatWin", &showThreatWindow_, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::End(); + return; + } + + if (!list || list->empty()) { + ImGui::TextDisabled("No threat data for current target."); + ImGui::End(); + return; + } + + uint32_t maxThreat = list->front().threat; + + ImGui::TextDisabled("%-19s Threat", "Player"); + ImGui::Separator(); + + uint64_t playerGuid = gameHandler.getPlayerGuid(); + int rank = 0; + for (const auto& entry : *list) { + ++rank; + bool isPlayer = (entry.victimGuid == playerGuid); + + // Resolve name + std::string victimName; + auto entity = gameHandler.getEntityManager().getEntity(entry.victimGuid); + if (entity) { + if (entity->getType() == game::ObjectType::PLAYER) { + auto p = std::static_pointer_cast(entity); + victimName = p->getName().empty() ? "Player" : p->getName(); + } else if (entity->getType() == game::ObjectType::UNIT) { + auto u = std::static_pointer_cast(entity); + victimName = u->getName().empty() ? "NPC" : u->getName(); + } + } + if (victimName.empty()) + victimName = "0x" + [&](){ + char buf[20]; snprintf(buf, sizeof(buf), "%llX", + static_cast(entry.victimGuid)); return std::string(buf); }(); + + // Colour: gold for #1 (tank), red if player is highest, white otherwise + ImVec4 col = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + if (rank == 1) col = ImVec4(1.0f, 0.82f, 0.0f, 1.0f); // gold + if (isPlayer && rank == 1) col = ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // red — you have aggro + + // Threat bar + float pct = (maxThreat > 0) ? (float)entry.threat / (float)maxThreat : 0.0f; + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, + isPlayer ? ImVec4(0.8f, 0.2f, 0.2f, 0.7f) : ImVec4(0.2f, 0.5f, 0.8f, 0.5f)); + char barLabel[48]; + snprintf(barLabel, sizeof(barLabel), "%.0f%%", pct * 100.0f); + ImGui::ProgressBar(pct, ImVec2(60, 14), barLabel); + ImGui::PopStyleColor(); + ImGui::SameLine(); + + ImGui::TextColored(col, "%-18s %u", victimName.c_str(), entry.threat); + + if (rank >= 10) break; // cap display at 10 entries + } + + ImGui::End(); +} + // ─── Inspect Window ─────────────────────────────────────────────────────────── void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) { if (!showInspectWindow_) return;