diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index 5721155a..fcd50c3f 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace wowee { namespace game { @@ -52,6 +53,9 @@ struct ItemDef { 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) + // Generic stat pairs for non-primary stats (hit, crit, haste, AP, SP, etc.) + struct ExtraStat { uint32_t statType = 0; int32_t statValue = 0; }; + std::vector extraStats; }; struct ItemSlot { diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 65a49430..eef6dba5 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1563,6 +1563,9 @@ struct ItemQueryResponseData { std::array spells{}; uint32_t bindType = 0; // 0=none, 1=BoP, 2=BoE, 3=BoU, 4=BoQ std::string description; // Flavor/lore text + // Generic stat pairs for non-primary stats (hit, crit, haste, AP, SP, etc.) + struct ExtraStat { uint32_t statType = 0; int32_t statValue = 0; }; + std::vector extraStats; bool valid = false; }; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 040eeb48..8677e9b5 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -10763,6 +10763,9 @@ void GameHandler::rebuildOnlineInventory() { def.requiredLevel = infoIt->second.requiredLevel; def.bindType = infoIt->second.bindType; def.description = infoIt->second.description; + def.extraStats.clear(); + for (const auto& es : infoIt->second.extraStats) + def.extraStats.push_back({es.statType, es.statValue}); } else { def.name = "Item " + std::to_string(def.itemId); queryItemInfo(def.itemId, guid); @@ -10808,6 +10811,9 @@ void GameHandler::rebuildOnlineInventory() { def.requiredLevel = infoIt->second.requiredLevel; def.bindType = infoIt->second.bindType; def.description = infoIt->second.description; + def.extraStats.clear(); + for (const auto& es : infoIt->second.extraStats) + def.extraStats.push_back({es.statType, es.statValue}); } else { def.name = "Item " + std::to_string(def.itemId); queryItemInfo(def.itemId, guid); @@ -10886,6 +10892,11 @@ 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; + def.extraStats.clear(); + for (const auto& es : infoIt->second.extraStats) + def.extraStats.push_back({es.statType, es.statValue}); def.bagSlots = infoIt->second.containerSlots; } else { def.name = "Item " + std::to_string(def.itemId); @@ -10932,6 +10943,9 @@ void GameHandler::rebuildOnlineInventory() { def.requiredLevel = infoIt->second.requiredLevel; def.bindType = infoIt->second.bindType; def.description = infoIt->second.description; + def.extraStats.clear(); + for (const auto& es : infoIt->second.extraStats) + def.extraStats.push_back({es.statType, es.statValue}); def.sellPrice = infoIt->second.sellPrice; def.bagSlots = infoIt->second.containerSlots; } else { @@ -11018,6 +11032,11 @@ void GameHandler::rebuildOnlineInventory() { def.itemLevel = infoIt->second.itemLevel; def.requiredLevel = infoIt->second.requiredLevel; def.sellPrice = infoIt->second.sellPrice; + def.bindType = infoIt->second.bindType; + def.description = infoIt->second.description; + def.extraStats.clear(); + for (const auto& es : infoIt->second.extraStats) + def.extraStats.push_back({es.statType, es.statValue}); def.bagSlots = infoIt->second.containerSlots; } else { def.name = "Item " + std::to_string(def.itemId); diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 5863c377..9cf0d570 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -1266,7 +1266,10 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ case 5: data.intellect = statValue; break; case 6: data.spirit = statValue; break; case 7: data.stamina = statValue; break; - default: break; + default: + if (statValue != 0) + data.extraStats.push_back({statType, statValue}); + break; } } } diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 65cdd913..44626ce2 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -931,7 +931,10 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery case 5: data.intellect = statValue; break; case 6: data.spirit = statValue; break; case 7: data.stamina = statValue; break; - default: break; + default: + if (statValue != 0) + data.extraStats.push_back({statType, statValue}); + break; } } // TBC: NO ScalingStatDistribution, NO ScalingStatValue (WotLK-only) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 2f31b774..4668d821 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2474,7 +2474,10 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa case 5: data.intellect = statValue; break; case 6: data.spirit = statValue; break; case 7: data.stamina = statValue; break; - default: break; + default: + if (statValue != 0) + data.extraStats.push_back({statType, statValue}); + break; } } diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 705acf52..2754a60b 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -1833,6 +1833,52 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I if (!bonusLine.empty()) { ImGui::TextColored(green, "%s", bonusLine.c_str()); } + + // Extra stats (hit, crit, haste, AP, SP, etc.) — one line each + for (const auto& es : item.extraStats) { + const char* statName = nullptr; + switch (es.statType) { + case 0: statName = "Mana"; break; + case 1: statName = "Health"; break; + case 12: statName = "Defense Rating"; break; + case 13: statName = "Dodge Rating"; break; + case 14: statName = "Parry Rating"; break; + case 15: statName = "Block Rating"; break; + case 16: statName = "Hit Rating"; break; + case 17: statName = "Hit Rating"; break; + case 18: statName = "Hit Rating"; break; + case 19: statName = "Crit Rating"; break; + case 20: statName = "Crit Rating"; break; + case 21: statName = "Crit Rating"; break; + case 28: statName = "Haste Rating"; break; + case 29: statName = "Haste Rating"; break; + case 30: statName = "Haste Rating"; break; + case 31: statName = "Hit Rating"; break; + case 32: statName = "Crit Rating"; break; + case 35: statName = "Resilience"; break; + case 36: statName = "Haste Rating"; break; + case 37: statName = "Expertise Rating"; break; + case 38: statName = "Attack Power"; break; + case 39: statName = "Ranged Attack Power"; break; + case 41: statName = "Healing Power"; break; + case 42: statName = "Spell Damage"; break; + case 43: statName = "Mana per 5 sec"; break; + case 44: statName = "Armor Penetration"; break; + case 45: statName = "Spell Power"; break; + case 46: statName = "Health per 5 sec"; break; + case 47: statName = "Spell Penetration"; break; + case 48: statName = "Block Value"; break; + default: statName = nullptr; break; + } + char buf[64]; + if (statName) { + std::snprintf(buf, sizeof(buf), "%+d %s", es.statValue, statName); + } else { + std::snprintf(buf, sizeof(buf), "%+d (stat %u)", es.statValue, es.statType); + } + ImGui::TextColored(green, "%s", buf); + } + if (item.requiredLevel > 1) { ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "Requires Level %u", item.requiredLevel); }