diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 1daf0492..556fdf46 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -758,6 +758,19 @@ public: void setAutoLoot(bool enabled) { autoLoot_ = enabled; } bool isAutoLoot() const { return autoLoot_; } + // Group loot roll + struct LootRollEntry { + uint64_t objectGuid = 0; + uint32_t slot = 0; + uint32_t itemId = 0; + std::string itemName; + uint8_t itemQuality = 0; + }; + bool hasPendingLootRoll() const { return pendingLootRollActive_; } + const LootRollEntry& getPendingLootRoll() const { return pendingLootRoll_; } + void sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollType); + // rollType: 0=need, 1=greed, 2=disenchant, 96=pass + // NPC Gossip void interactWithNpc(uint64_t guid); void interactWithGameObject(uint64_t guid); @@ -1258,6 +1271,8 @@ private: void handleDuelRequested(network::Packet& packet); void handleDuelComplete(network::Packet& packet); void handleDuelWinner(network::Packet& packet); + void handleLootRoll(network::Packet& packet); + void handleLootRollWon(network::Packet& packet); // ---- LFG / Dungeon Finder handlers ---- void handleLfgJoinResult(network::Packet& packet); @@ -1637,6 +1652,10 @@ private: bool lootWindowOpen = false; bool autoLoot_ = false; LootResponseData currentLoot; + + // Group loot roll state + bool pendingLootRollActive_ = false; + LootRollEntry pendingLootRoll_; struct LocalLootState { LootResponseData data; bool moneyTaken = false; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 6cc922f8..6cd82c52 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -205,6 +205,7 @@ private: void renderPartyFrames(game::GameHandler& gameHandler); void renderGroupInvitePopup(game::GameHandler& gameHandler); void renderDuelRequestPopup(game::GameHandler& gameHandler); + void renderLootRollPopup(game::GameHandler& gameHandler); void renderBuffBar(game::GameHandler& gameHandler); void renderLootWindow(game::GameHandler& gameHandler); void renderGossipWindow(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 7e114002..e39a6c74 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1953,6 +1953,16 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_LOOT_REMOVED: handleLootRemoved(packet); break; + case Opcode::SMSG_LOOT_ROLL: + handleLootRoll(packet); + break; + case Opcode::SMSG_LOOT_ROLL_WON: + handleLootRollWon(packet); + break; + case Opcode::SMSG_LOOT_MASTER_LIST: + // Master looter list — no UI yet; consume to avoid unhandled warning. + packet.setReadPos(packet.getSize()); + break; case Opcode::SMSG_GOSSIP_MESSAGE: handleGossipMessage(packet); break; @@ -14948,6 +14958,122 @@ void GameHandler::handleAuctionCommandResult(network::Packet& packet) { " error=", result.errorCode); } +// --------------------------------------------------------------------------- +// Group loot roll (SMSG_LOOT_ROLL / SMSG_LOOT_ROLL_WON / CMSG_LOOT_ROLL) +// --------------------------------------------------------------------------- + +void GameHandler::handleLootRoll(network::Packet& packet) { + // uint64 objectGuid, uint32 slot, uint64 playerGuid, + // uint32 itemId, uint32 randomSuffix, uint32 randomPropId, + // uint8 rollNumber, uint8 rollType + size_t rem = packet.getSize() - packet.getReadPos(); + if (rem < 26) return; // minimum: 8+4+8+4+4+4+1+1 = 34, be lenient + + uint64_t objectGuid = packet.readUInt64(); + uint32_t slot = packet.readUInt32(); + uint64_t rollerGuid = packet.readUInt64(); + uint32_t itemId = packet.readUInt32(); + /*uint32_t randSuffix =*/ packet.readUInt32(); + /*uint32_t randProp =*/ packet.readUInt32(); + uint8_t rollNum = packet.readUInt8(); + uint8_t rollType = packet.readUInt8(); + + // rollType 128 = "waiting for this player to roll" + if (rollType == 128 && rollerGuid == playerGuid) { + // Server is asking us to roll; present the roll UI. + pendingLootRollActive_ = true; + pendingLootRoll_.objectGuid = objectGuid; + pendingLootRoll_.slot = slot; + pendingLootRoll_.itemId = itemId; + // Look up item name from cache + auto* info = getItemInfo(itemId); + pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId); + pendingLootRoll_.itemQuality = info ? static_cast(info->quality) : 0; + LOG_INFO("SMSG_LOOT_ROLL: need/greed prompt for item=", itemId, + " (", pendingLootRoll_.itemName, ") slot=", slot); + return; + } + + // Otherwise it's reporting another player's roll result + const char* rollNames[] = {"Need", "Greed", "Disenchant", "Pass"}; + const char* rollName = (rollType < 4) ? rollNames[rollType] : "Pass"; + + std::string rollerName; + auto entity = entityManager.getEntity(rollerGuid); + if (auto* unit = dynamic_cast(entity.get())) { + rollerName = unit->getName(); + } + if (rollerName.empty()) rollerName = "Someone"; + + auto* info = getItemInfo(itemId); + std::string iName = info ? info->name : std::to_string(itemId); + + char buf[256]; + std::snprintf(buf, sizeof(buf), "%s rolls %s (%d) on [%s]", + rollerName.c_str(), rollName, static_cast(rollNum), iName.c_str()); + addSystemChatMessage(buf); + + LOG_DEBUG("SMSG_LOOT_ROLL: ", rollerName, " rolled ", rollName, + " (", rollNum, ") on item ", itemId); + (void)objectGuid; (void)slot; +} + +void GameHandler::handleLootRollWon(network::Packet& packet) { + size_t rem = packet.getSize() - packet.getReadPos(); + if (rem < 26) return; + + /*uint64_t objectGuid =*/ packet.readUInt64(); + /*uint32_t slot =*/ packet.readUInt32(); + uint64_t winnerGuid = packet.readUInt64(); + uint32_t itemId = packet.readUInt32(); + /*uint32_t randSuffix =*/ packet.readUInt32(); + /*uint32_t randProp =*/ packet.readUInt32(); + uint8_t rollNum = packet.readUInt8(); + uint8_t rollType = packet.readUInt8(); + + const char* rollNames[] = {"Need", "Greed", "Disenchant"}; + const char* rollName = (rollType < 3) ? rollNames[rollType] : "Roll"; + + std::string winnerName; + auto entity = entityManager.getEntity(winnerGuid); + if (auto* unit = dynamic_cast(entity.get())) { + winnerName = unit->getName(); + } + if (winnerName.empty()) { + winnerName = (winnerGuid == playerGuid) ? "You" : "Someone"; + } + + auto* info = getItemInfo(itemId); + std::string iName = info ? info->name : std::to_string(itemId); + + char buf[256]; + std::snprintf(buf, sizeof(buf), "%s wins [%s] (%s %d)!", + winnerName.c_str(), iName.c_str(), rollName, static_cast(rollNum)); + addSystemChatMessage(buf); + + // Clear pending roll if it was ours + if (pendingLootRollActive_ && winnerGuid == playerGuid) { + pendingLootRollActive_ = false; + } + LOG_INFO("SMSG_LOOT_ROLL_WON: winner=", winnerName, " item=", itemId, + " roll=", rollName, "(", rollNum, ")"); +} + +void GameHandler::sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollType) { + if (state != WorldState::IN_WORLD || !socket) return; + pendingLootRollActive_ = false; + + network::Packet pkt(wireOpcode(Opcode::CMSG_LOOT_ROLL)); + pkt.writeUInt64(objectGuid); + pkt.writeUInt32(slot); + pkt.writeUInt8(rollType); + socket->send(pkt); + + const char* rollNames[] = {"Need", "Greed", "Disenchant", "Pass"}; + const char* rName = (rollType < 3) ? rollNames[rollType] : "Pass"; + LOG_INFO("CMSG_LOOT_ROLL: type=", rName, " item=", pendingLootRoll_.itemName); +} + // --------------------------------------------------------------------------- // SMSG_ACHIEVEMENT_EARNED (WotLK 3.3.5a wire 0x4AB) // uint64 guid — player who earned it (may be another player) diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index c652ba1c..93cab28d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -397,6 +397,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderPartyFrames(gameHandler); renderGroupInvitePopup(gameHandler); renderDuelRequestPopup(gameHandler); + renderLootRollPopup(gameHandler); renderGuildInvitePopup(gameHandler); renderGuildRoster(gameHandler); renderBuffBar(gameHandler); @@ -4401,6 +4402,53 @@ void GameScreen::renderDuelRequestPopup(game::GameHandler& gameHandler) { ImGui::End(); } +void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) { + if (!gameHandler.hasPendingLootRoll()) return; + + const auto& roll = gameHandler.getPendingLootRoll(); + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + + ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 310), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); + + if (ImGui::Begin("Loot Roll", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + // Quality color for item name + static const ImVec4 kQualityColors[] = { + ImVec4(0.6f, 0.6f, 0.6f, 1.0f), // 0=poor (grey) + ImVec4(1.0f, 1.0f, 1.0f, 1.0f), // 1=common (white) + ImVec4(0.1f, 1.0f, 0.1f, 1.0f), // 2=uncommon (green) + ImVec4(0.0f, 0.44f, 0.87f, 1.0f),// 3=rare (blue) + ImVec4(0.64f, 0.21f, 0.93f, 1.0f),// 4=epic (purple) + ImVec4(1.0f, 0.5f, 0.0f, 1.0f), // 5=legendary (orange) + }; + uint8_t q = roll.itemQuality; + ImVec4 col = (q < 6) ? kQualityColors[q] : kQualityColors[1]; + + ImGui::Text("An item is up for rolls:"); + ImGui::TextColored(col, "[%s]", roll.itemName.c_str()); + ImGui::Spacing(); + + if (ImGui::Button("Need", ImVec2(80, 30))) { + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 0); + } + ImGui::SameLine(); + if (ImGui::Button("Greed", ImVec2(80, 30))) { + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 1); + } + ImGui::SameLine(); + if (ImGui::Button("Disenchant", ImVec2(95, 30))) { + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 2); + } + ImGui::SameLine(); + if (ImGui::Button("Pass", ImVec2(70, 30))) { + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 96); + } + } + ImGui::End(); +} + void GameScreen::renderGuildInvitePopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingGuildInvite()) return;