mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-25 00:20:16 +00:00
feat: achievement name in toast, parse earned achievements, loot item tooltips
- Parse SMSG_ALL_ACHIEVEMENT_DATA on login to populate earnedAchievements_ set - Pass achievement name through callback so toast shows name instead of ID - Add renderItemTooltip(ItemQueryResponseData) overload for loot/non-inventory contexts - Loot window now shows full item tooltip on hover (stats, sell price, bind type, etc.)
This commit is contained in:
parent
984decd664
commit
6275a45ec0
7 changed files with 225 additions and 10 deletions
|
|
@ -1134,8 +1134,9 @@ public:
|
|||
void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); }
|
||||
|
||||
// Achievement earned callback — fires when SMSG_ACHIEVEMENT_EARNED is received
|
||||
using AchievementEarnedCallback = std::function<void(uint32_t achievementId)>;
|
||||
using AchievementEarnedCallback = std::function<void(uint32_t achievementId, const std::string& name)>;
|
||||
void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); }
|
||||
const std::unordered_set<uint32_t>& getEarnedAchievements() const { return earnedAchievements_; }
|
||||
|
||||
// Server-triggered music callback — fires when SMSG_PLAY_MUSIC is received.
|
||||
// The soundId corresponds to a SoundEntries.dbc record. The receiver is
|
||||
|
|
@ -2246,6 +2247,9 @@ private:
|
|||
std::unordered_map<uint32_t, std::string> achievementNameCache_;
|
||||
bool achievementNameCacheLoaded_ = false;
|
||||
void loadAchievementNameCache();
|
||||
// Set of achievement IDs earned by the player (populated from SMSG_ALL_ACHIEVEMENT_DATA)
|
||||
std::unordered_set<uint32_t> earnedAchievements_;
|
||||
void handleAllAchievementData(network::Packet& packet);
|
||||
|
||||
// Area name cache (lazy-loaded from WorldMapArea.dbc; maps AreaTable ID → display name)
|
||||
std::unordered_map<uint32_t, std::string> areaNameCache_;
|
||||
|
|
|
|||
|
|
@ -366,6 +366,7 @@ private:
|
|||
static constexpr float ACHIEVEMENT_TOAST_DURATION = 5.0f;
|
||||
float achievementToastTimer_ = 0.0f;
|
||||
uint32_t achievementToastId_ = 0;
|
||||
std::string achievementToastName_;
|
||||
void renderAchievementToast();
|
||||
|
||||
// Zone discovery text ("Entering: <ZoneName>")
|
||||
|
|
@ -377,7 +378,7 @@ private:
|
|||
|
||||
public:
|
||||
void triggerDing(uint32_t newLevel);
|
||||
void triggerAchievementToast(uint32_t achievementId);
|
||||
void triggerAchievementToast(uint32_t achievementId, std::string name = {});
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ private:
|
|||
std::unordered_map<uint32_t, VkDescriptorSet> iconCache_;
|
||||
public:
|
||||
VkDescriptorSet getItemIcon(uint32_t displayInfoId);
|
||||
void renderItemTooltip(const game::ItemQueryResponseData& info);
|
||||
private:
|
||||
|
||||
// Character model preview
|
||||
|
|
|
|||
|
|
@ -2335,9 +2335,9 @@ void Application::setupUICallbacks() {
|
|||
});
|
||||
|
||||
// Achievement earned callback — show toast banner
|
||||
gameHandler->setAchievementEarnedCallback([this](uint32_t achievementId) {
|
||||
gameHandler->setAchievementEarnedCallback([this](uint32_t achievementId, const std::string& name) {
|
||||
if (uiManager) {
|
||||
uiManager->getGameScreen().triggerAchievementToast(achievementId);
|
||||
uiManager->getGameScreen().triggerAchievementToast(achievementId, name);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2695,7 +2695,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleAchievementEarned(packet);
|
||||
break;
|
||||
case Opcode::SMSG_ALL_ACHIEVEMENT_DATA:
|
||||
// Initial data burst on login — ignored for now (no achievement tracker UI).
|
||||
handleAllAchievementData(packet);
|
||||
break;
|
||||
case Opcode::SMSG_ITEM_COOLDOWN: {
|
||||
// uint64 itemGuid + uint32 spellId + uint32 cooldownMs
|
||||
|
|
@ -18711,8 +18711,9 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) {
|
|||
}
|
||||
addSystemChatMessage(buf);
|
||||
|
||||
earnedAchievements_.insert(achievementId);
|
||||
if (achievementEarnedCallback_) {
|
||||
achievementEarnedCallback_(achievementId);
|
||||
achievementEarnedCallback_(achievementId, achName);
|
||||
}
|
||||
} else {
|
||||
// Another player in the zone earned an achievement
|
||||
|
|
@ -18743,6 +18744,38 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) {
|
|||
achName.empty() ? "" : " name=", achName);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SMSG_ALL_ACHIEVEMENT_DATA (WotLK 3.3.5a)
|
||||
// Achievement records: repeated { uint32 id, uint32 packedDate } until 0xFFFFFFFF sentinel
|
||||
// Criteria records: repeated { uint32 id, uint64 counter, uint32 packedDate, ... } until 0xFFFFFFFF
|
||||
// ---------------------------------------------------------------------------
|
||||
void GameHandler::handleAllAchievementData(network::Packet& packet) {
|
||||
loadAchievementNameCache();
|
||||
earnedAchievements_.clear();
|
||||
|
||||
// Parse achievement entries (id + packedDate pairs, sentinel 0xFFFFFFFF)
|
||||
while (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
uint32_t id = packet.readUInt32();
|
||||
if (id == 0xFFFFFFFF) break;
|
||||
if (packet.getSize() - packet.getReadPos() < 4) break;
|
||||
/*uint32_t date =*/ packet.readUInt32();
|
||||
earnedAchievements_.insert(id);
|
||||
}
|
||||
|
||||
// Skip criteria block (id + uint64 counter + uint32 date + uint32 flags until 0xFFFFFFFF)
|
||||
while (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
uint32_t id = packet.readUInt32();
|
||||
if (id == 0xFFFFFFFF) break;
|
||||
// counter(8) + date(4) + unknown(4) = 16 bytes
|
||||
if (packet.getSize() - packet.getReadPos() < 16) break;
|
||||
packet.readUInt64(); // counter
|
||||
packet.readUInt32(); // date
|
||||
packet.readUInt32(); // unknown / flags
|
||||
}
|
||||
|
||||
LOG_INFO("SMSG_ALL_ACHIEVEMENT_DATA: loaded ", earnedAchievements_.size(), " earned achievements");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Faction name cache (lazily loaded from Faction.dbc)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -6499,6 +6499,13 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
|
||||
// Show item tooltip on hover
|
||||
if (hovered && info && info->valid) {
|
||||
inventoryScreen.renderItemTooltip(*info);
|
||||
} else if (hovered && !itemName.empty() && itemName[0] != 'I') {
|
||||
ImGui::SetTooltip("%s", itemName.c_str());
|
||||
}
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
// Draw hover highlight
|
||||
|
|
@ -10799,8 +10806,9 @@ void GameScreen::renderDingEffect() {
|
|||
IM_COL32(255, 210, 0, (int)(alpha * 255)), buf);
|
||||
}
|
||||
|
||||
void GameScreen::triggerAchievementToast(uint32_t achievementId) {
|
||||
void GameScreen::triggerAchievementToast(uint32_t achievementId, std::string name) {
|
||||
achievementToastId_ = achievementId;
|
||||
achievementToastName_ = std::move(name);
|
||||
achievementToastTimer_ = ACHIEVEMENT_TOAST_DURATION;
|
||||
|
||||
// Play a UI sound if available
|
||||
|
|
@ -10859,9 +10867,15 @@ void GameScreen::renderAchievementToast() {
|
|||
draw->AddText(font, titleSize, ImVec2(titleX, toastY + 8),
|
||||
IM_COL32(255, 215, 0, (int)(alpha * 255)), title);
|
||||
|
||||
// Achievement ID line (until we have Achievement.dbc name lookup)
|
||||
char idBuf[64];
|
||||
std::snprintf(idBuf, sizeof(idBuf), "Achievement #%u", achievementToastId_);
|
||||
// Achievement name (falls back to ID if name not available)
|
||||
char idBuf[256];
|
||||
const char* achText = achievementToastName_.empty()
|
||||
? nullptr : achievementToastName_.c_str();
|
||||
if (achText) {
|
||||
std::snprintf(idBuf, sizeof(idBuf), "%s", achText);
|
||||
} else {
|
||||
std::snprintf(idBuf, sizeof(idBuf), "Achievement #%u", achievementToastId_);
|
||||
}
|
||||
float idW = font->CalcTextSizeA(bodySize, FLT_MAX, 0.0f, idBuf).x;
|
||||
float idX = toastX + (TOAST_W - idW) * 0.5f;
|
||||
draw->AddText(font, bodySize, ImVec2(idX, toastY + 28),
|
||||
|
|
|
|||
|
|
@ -2027,5 +2027,167 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tooltip overload for ItemQueryResponseData (used by loot window, etc.)
|
||||
// ---------------------------------------------------------------------------
|
||||
void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info) {
|
||||
ImGui::BeginTooltip();
|
||||
|
||||
ImVec4 qColor = getQualityColor(static_cast<game::ItemQuality>(info.quality));
|
||||
ImGui::TextColored(qColor, "%s", info.name.c_str());
|
||||
if (info.itemLevel > 0) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "Item Level %u", info.itemLevel);
|
||||
}
|
||||
|
||||
// Binding type
|
||||
switch (info.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;
|
||||
}
|
||||
|
||||
// Slot / subclass
|
||||
if (info.inventoryType > 0) {
|
||||
const char* slotName = "";
|
||||
switch (info.inventoryType) {
|
||||
case 1: slotName = "Head"; break;
|
||||
case 2: slotName = "Neck"; break;
|
||||
case 3: slotName = "Shoulder"; break;
|
||||
case 4: slotName = "Shirt"; break;
|
||||
case 5: slotName = "Chest"; break;
|
||||
case 6: slotName = "Waist"; break;
|
||||
case 7: slotName = "Legs"; break;
|
||||
case 8: slotName = "Feet"; break;
|
||||
case 9: slotName = "Wrist"; break;
|
||||
case 10: slotName = "Hands"; break;
|
||||
case 11: slotName = "Finger"; break;
|
||||
case 12: slotName = "Trinket"; break;
|
||||
case 13: slotName = "One-Hand"; break;
|
||||
case 14: slotName = "Shield"; break;
|
||||
case 15: slotName = "Ranged"; break;
|
||||
case 16: slotName = "Back"; break;
|
||||
case 17: slotName = "Two-Hand"; break;
|
||||
case 18: slotName = "Bag"; break;
|
||||
case 19: slotName = "Tabard"; break;
|
||||
case 20: slotName = "Robe"; break;
|
||||
case 21: slotName = "Main Hand"; break;
|
||||
case 22: slotName = "Off Hand"; break;
|
||||
case 23: slotName = "Held In Off-hand"; break;
|
||||
case 25: slotName = "Thrown"; break;
|
||||
case 26: slotName = "Ranged"; break;
|
||||
default: break;
|
||||
}
|
||||
if (slotName[0]) {
|
||||
if (!info.subclassName.empty())
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s %s", slotName, info.subclassName.c_str());
|
||||
else
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName);
|
||||
}
|
||||
}
|
||||
|
||||
// Weapon stats
|
||||
auto isWeaponInvType = [](uint32_t t) {
|
||||
return t == 13 || t == 15 || t == 17 || t == 21 || t == 25 || t == 26;
|
||||
};
|
||||
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
if (isWeaponInvType(info.inventoryType) && info.damageMax > 0.0f && info.delayMs > 0) {
|
||||
float speed = static_cast<float>(info.delayMs) / 1000.0f;
|
||||
float dps = ((info.damageMin + info.damageMax) * 0.5f) / speed;
|
||||
ImGui::Text("%.0f - %.0f Damage", info.damageMin, info.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);
|
||||
}
|
||||
|
||||
if (info.armor > 0) ImGui::Text("%d Armor", info.armor);
|
||||
|
||||
auto appendBonus = [](std::string& out, int32_t val, const char* name) {
|
||||
if (val <= 0) return;
|
||||
if (!out.empty()) out += " ";
|
||||
out += "+" + std::to_string(val) + " " + name;
|
||||
};
|
||||
std::string bonusLine;
|
||||
appendBonus(bonusLine, info.strength, "Str");
|
||||
appendBonus(bonusLine, info.agility, "Agi");
|
||||
appendBonus(bonusLine, info.stamina, "Sta");
|
||||
appendBonus(bonusLine, info.intellect, "Int");
|
||||
appendBonus(bonusLine, info.spirit, "Spi");
|
||||
if (!bonusLine.empty()) ImGui::TextColored(green, "%s", bonusLine.c_str());
|
||||
|
||||
// Extra stats
|
||||
for (const auto& es : info.extraStats) {
|
||||
const char* statName = nullptr;
|
||||
switch (es.statType) {
|
||||
case 12: statName = "Defense Rating"; break;
|
||||
case 13: statName = "Dodge Rating"; break;
|
||||
case 14: statName = "Parry Rating"; break;
|
||||
case 16: case 17: case 18: case 31: statName = "Hit Rating"; break;
|
||||
case 19: case 20: case 21: case 32: statName = "Crit Rating"; break;
|
||||
case 28: case 29: case 30: case 36: statName = "Haste Rating"; break;
|
||||
case 35: statName = "Resilience"; 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 (info.requiredLevel > 1) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "Requires Level %u", info.requiredLevel);
|
||||
}
|
||||
|
||||
// Spell effects
|
||||
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;
|
||||
default: break;
|
||||
}
|
||||
if (!trigger) continue;
|
||||
if (gameHandler_) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (info.startQuestId != 0) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Begins a Quest");
|
||||
}
|
||||
if (!info.description.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.5f, 0.9f), "\"%s\"", info.description.c_str());
|
||||
}
|
||||
|
||||
if (info.sellPrice > 0) {
|
||||
uint32_t g = info.sellPrice / 10000;
|
||||
uint32_t s = (info.sellPrice / 100) % 100;
|
||||
uint32_t c = info.sellPrice % 100;
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", g, s, c);
|
||||
}
|
||||
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue