From 76bd6b409eee964d23470b9204d1465595cd6bd1 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 16:47:55 -0700 Subject: [PATCH] feat: enhance item tooltips with binding, description, speed, and spell effects - Parse Bonding and Description fields from SMSG_ITEM_QUERY_SINGLE_RESPONSE (read after the 5 spell slots: bindType uint32, then description cstring) - Add bindType and description to ItemQueryResponseData and ItemDef - Propagate bindType and description through all 5 rebuildOnlineInventory paths - Tooltip now shows: "Binds when picked up/equipped/used/quest item" - Tooltip now shows weapon damage range ("X - Y Damage") and speed ("Speed 2.60") on same line, plus DPS in parentheses below - Tooltip now shows spell effects ("Use: ", "Equip: ", "Chance on Hit: ...") using existing getSpellName() lookup - Tooltip now shows item flavor/lore description in italic-style yellow text --- include/game/inventory.hpp | 2 ++ include/game/world_packets.hpp | 2 ++ src/game/game_handler.cpp | 6 +++++ src/game/world_packets.cpp | 8 ++++++ src/ui/inventory_screen.cpp | 49 +++++++++++++++++++++++++++++++--- 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index 88b7db38..5721155a 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -50,6 +50,8 @@ struct ItemDef { uint32_t maxDurability = 0; uint32_t itemLevel = 0; uint32_t requiredLevel = 0; + uint32_t bindType = 0; // 0=none, 1=BoP, 2=BoE, 3=BoU, 4=BoQ + std::string description; // Flavor/lore text shown in tooltip (italic yellow) }; struct ItemSlot { diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 03fdef5c..65a49430 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1561,6 +1561,8 @@ struct ItemQueryResponseData { uint32_t spellTrigger = 0; // 0=Use, 1=Equip, 2=ChanceOnHit, 5=Learn }; std::array spells{}; + uint32_t bindType = 0; // 0=none, 1=BoP, 2=BoE, 3=BoU, 4=BoQ + std::string description; // Flavor/lore text bool valid = false; }; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ab490ea7..040eeb48 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -10761,6 +10761,8 @@ void GameHandler::rebuildOnlineInventory() { def.sellPrice = infoIt->second.sellPrice; def.itemLevel = infoIt->second.itemLevel; def.requiredLevel = infoIt->second.requiredLevel; + def.bindType = infoIt->second.bindType; + def.description = infoIt->second.description; } else { def.name = "Item " + std::to_string(def.itemId); queryItemInfo(def.itemId, guid); @@ -10804,6 +10806,8 @@ void GameHandler::rebuildOnlineInventory() { def.sellPrice = infoIt->second.sellPrice; def.itemLevel = infoIt->second.itemLevel; def.requiredLevel = infoIt->second.requiredLevel; + def.bindType = infoIt->second.bindType; + def.description = infoIt->second.description; } else { def.name = "Item " + std::to_string(def.itemId); queryItemInfo(def.itemId, guid); @@ -10926,6 +10930,8 @@ void GameHandler::rebuildOnlineInventory() { def.spirit = infoIt->second.spirit; def.itemLevel = infoIt->second.itemLevel; def.requiredLevel = infoIt->second.requiredLevel; + def.bindType = infoIt->second.bindType; + def.description = infoIt->second.description; def.sellPrice = infoIt->second.sellPrice; def.bagSlots = infoIt->second.containerSlots; } else { diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index beb98e6c..2f31b774 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2518,6 +2518,14 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa packet.readUInt32(); // SpellCategoryCooldown } + // Bonding type (0=none, 1=BoP, 2=BoE, 3=BoU, 4=BoQ) + if (packet.getReadPos() + 4 <= packet.getSize()) + data.bindType = packet.readUInt32(); + + // Flavor/lore text (Description cstring) + if (packet.getReadPos() < packet.getSize()) + data.description = packet.readString(); + data.valid = !data.name.empty(); return true; } diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 54d8abab..705acf52 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -1718,6 +1718,15 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "Item Level %u", item.itemLevel); } + // Binding type + switch (item.bindType) { + case 1: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when picked up"); break; + case 2: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when equipped"); break; + case 3: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when used"); break; + case 4: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Quest Item"); break; + default: break; + } + if (item.itemId == 6948 && gameHandler_) { uint32_t mapId = 0; glm::vec3 pos; @@ -1793,13 +1802,15 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I }; const bool isWeapon = isWeaponInventoryType(item.inventoryType); - // Compact stats view for weapons: DPS + condensed stat bonuses. - // Non-weapons keep armor/sell info visible. + // Compact stats view for weapons: damage range + speed + DPS ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f); if (isWeapon && item.damageMax > 0.0f && item.delayMs > 0) { float speed = static_cast(item.delayMs) / 1000.0f; float dps = ((item.damageMin + item.damageMax) * 0.5f) / speed; - ImGui::Text("%.1f DPS", dps); + ImGui::Text("%.0f - %.0f Damage", item.damageMin, item.damageMax); + ImGui::SameLine(160.0f); + ImGui::TextDisabled("Speed %.2f", speed); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(%.1f damage per second)", dps); } // Armor appears before stat bonuses — matches WoW tooltip order @@ -1834,6 +1845,38 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I ImGui::TextColored(durColor, "Durability %u / %u", item.curDurability, item.maxDurability); } + // Item spell effects (Use/Equip/Chance on Hit) + if (gameHandler_) { + auto* info = gameHandler_->getItemInfo(item.itemId); + if (info) { + for (const auto& sp : info->spells) { + if (sp.spellId == 0) continue; + const char* trigger = nullptr; + switch (sp.spellTrigger) { + case 0: trigger = "Use"; break; + case 1: trigger = "Equip"; break; + case 2: trigger = "Chance on Hit"; break; + case 6: trigger = "Soulstone"; break; + default: break; + } + if (!trigger) continue; + const std::string& spName = gameHandler_->getSpellName(sp.spellId); + if (!spName.empty()) { + ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), + "%s: %s", trigger, spName.c_str()); + } else { + ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), + "%s: Spell #%u", trigger, sp.spellId); + } + } + } + } + + // Flavor / lore text (italic yellow in WoW, just yellow here) + if (!item.description.empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.5f, 0.9f), "\"%s\"", item.description.c_str()); + } + if (item.sellPrice > 0) { uint32_t g = item.sellPrice / 10000; uint32_t s = (item.sellPrice / 100) % 100;