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: <SpellName>", "Equip: <SpellName>",
  "Chance on Hit: ...") using existing getSpellName() lookup
- Tooltip now shows item flavor/lore description in italic-style yellow text
This commit is contained in:
Kelsi 2026-03-10 16:47:55 -07:00
parent f53f16a59b
commit 76bd6b409e
5 changed files with 64 additions and 3 deletions

View file

@ -50,6 +50,8 @@ struct ItemDef {
uint32_t maxDurability = 0; uint32_t maxDurability = 0;
uint32_t itemLevel = 0; uint32_t itemLevel = 0;
uint32_t requiredLevel = 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 { struct ItemSlot {

View file

@ -1561,6 +1561,8 @@ struct ItemQueryResponseData {
uint32_t spellTrigger = 0; // 0=Use, 1=Equip, 2=ChanceOnHit, 5=Learn uint32_t spellTrigger = 0; // 0=Use, 1=Equip, 2=ChanceOnHit, 5=Learn
}; };
std::array<ItemSpell, 5> spells{}; std::array<ItemSpell, 5> spells{};
uint32_t bindType = 0; // 0=none, 1=BoP, 2=BoE, 3=BoU, 4=BoQ
std::string description; // Flavor/lore text
bool valid = false; bool valid = false;
}; };

View file

@ -10761,6 +10761,8 @@ void GameHandler::rebuildOnlineInventory() {
def.sellPrice = infoIt->second.sellPrice; def.sellPrice = infoIt->second.sellPrice;
def.itemLevel = infoIt->second.itemLevel; def.itemLevel = infoIt->second.itemLevel;
def.requiredLevel = infoIt->second.requiredLevel; def.requiredLevel = infoIt->second.requiredLevel;
def.bindType = infoIt->second.bindType;
def.description = infoIt->second.description;
} else { } else {
def.name = "Item " + std::to_string(def.itemId); def.name = "Item " + std::to_string(def.itemId);
queryItemInfo(def.itemId, guid); queryItemInfo(def.itemId, guid);
@ -10804,6 +10806,8 @@ void GameHandler::rebuildOnlineInventory() {
def.sellPrice = infoIt->second.sellPrice; def.sellPrice = infoIt->second.sellPrice;
def.itemLevel = infoIt->second.itemLevel; def.itemLevel = infoIt->second.itemLevel;
def.requiredLevel = infoIt->second.requiredLevel; def.requiredLevel = infoIt->second.requiredLevel;
def.bindType = infoIt->second.bindType;
def.description = infoIt->second.description;
} else { } else {
def.name = "Item " + std::to_string(def.itemId); def.name = "Item " + std::to_string(def.itemId);
queryItemInfo(def.itemId, guid); queryItemInfo(def.itemId, guid);
@ -10926,6 +10930,8 @@ void GameHandler::rebuildOnlineInventory() {
def.spirit = infoIt->second.spirit; def.spirit = infoIt->second.spirit;
def.itemLevel = infoIt->second.itemLevel; def.itemLevel = infoIt->second.itemLevel;
def.requiredLevel = infoIt->second.requiredLevel; def.requiredLevel = infoIt->second.requiredLevel;
def.bindType = infoIt->second.bindType;
def.description = infoIt->second.description;
def.sellPrice = infoIt->second.sellPrice; def.sellPrice = infoIt->second.sellPrice;
def.bagSlots = infoIt->second.containerSlots; def.bagSlots = infoIt->second.containerSlots;
} else { } else {

View file

@ -2518,6 +2518,14 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
packet.readUInt32(); // SpellCategoryCooldown 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(); data.valid = !data.name.empty();
return true; return true;
} }

View file

@ -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); 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_) { if (item.itemId == 6948 && gameHandler_) {
uint32_t mapId = 0; uint32_t mapId = 0;
glm::vec3 pos; glm::vec3 pos;
@ -1793,13 +1802,15 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
}; };
const bool isWeapon = isWeaponInventoryType(item.inventoryType); const bool isWeapon = isWeaponInventoryType(item.inventoryType);
// Compact stats view for weapons: DPS + condensed stat bonuses. // Compact stats view for weapons: damage range + speed + DPS
// Non-weapons keep armor/sell info visible.
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f); ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
if (isWeapon && item.damageMax > 0.0f && item.delayMs > 0) { if (isWeapon && item.damageMax > 0.0f && item.delayMs > 0) {
float speed = static_cast<float>(item.delayMs) / 1000.0f; float speed = static_cast<float>(item.delayMs) / 1000.0f;
float dps = ((item.damageMin + item.damageMax) * 0.5f) / speed; 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 // 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", ImGui::TextColored(durColor, "Durability %u / %u",
item.curDurability, item.maxDurability); 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) { if (item.sellPrice > 0) {
uint32_t g = item.sellPrice / 10000; uint32_t g = item.sellPrice / 10000;
uint32_t s = (item.sellPrice / 100) % 100; uint32_t s = (item.sellPrice / 100) % 100;