From 42d66bc876ab7e69e1e71f1d60b195ef0bf9aa81 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 16:24:11 -0700 Subject: [PATCH] feat: show quality-coloured loot toast when items are received SMSG_ITEM_PUSH_RESULT now fires a new ItemLootCallback that game_screen.cpp uses to push a compact slide-in toast at the bottom-left of the screen. Each toast: - Shows a quality-tinted left accent bar (grey/white/green/blue/ purple/orange matching WoW quality colours) - Displays "Loot: " with the name in quality colour - Appends " x" for stacked pickups - Coalesces repeated pickups of the same item (adds count, resets timer) - Stacks up to 5 entries, 3 s lifetime with 0.15 s slide-in and 0.7 s fade-out --- include/game/game_handler.hpp | 7 +++ include/ui/game_screen.hpp | 13 +++++ src/game/game_handler.cpp | 5 ++ src/ui/game_screen.cpp | 107 ++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 501a0f73..9c1015a7 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1494,6 +1494,10 @@ public: using PvpHonorCallback = std::function; void setPvpHonorCallback(PvpHonorCallback cb) { pvpHonorCallback_ = std::move(cb); } + // Item looted / received callback (SMSG_ITEM_PUSH_RESULT when showInChat is set) + using ItemLootCallback = std::function; + void setItemLootCallback(ItemLootCallback cb) { itemLootCallback_ = std::move(cb); } + // Quest turn-in completion callback using QuestCompleteCallback = std::function; void setQuestCompleteCallback(QuestCompleteCallback cb) { questCompleteCallback_ = std::move(cb); } @@ -2838,6 +2842,9 @@ private: // ---- PvP honor credit callback ---- PvpHonorCallback pvpHonorCallback_; + // ---- Item loot callback ---- + ItemLootCallback itemLootCallback_; + // ---- Quest completion callback ---- QuestCompleteCallback questCompleteCallback_; }; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 77fc01d8..7fe1b72e 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -574,6 +574,19 @@ private: bool pvpHonorCallbackSet_ = false; void renderPvpHonorToasts(); + // Item loot toast — quality-coloured popup when an item is received + struct ItemLootToastEntry { + uint32_t itemId = 0; + uint32_t count = 0; + uint32_t quality = 1; // 0=grey,1=white,2=green,3=blue,4=purple,5=orange + std::string name; + float age = 0.0f; + }; + static constexpr float ITEM_LOOT_TOAST_DURATION = 3.0f; + std::vector itemLootToasts_; + bool itemLootCallbackSet_ = false; + void renderItemLootToasts(); + // Zone discovery text ("Entering: ") static constexpr float ZONE_TEXT_DURATION = 5.0f; float zoneTextTimer_ = 0.0f; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index e7911baf..f70a3c39 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1701,12 +1701,17 @@ void GameHandler::handlePacket(network::Packet& packet) { queryItemInfo(itemId, 0); if (showInChat) { std::string itemName = "item #" + std::to_string(itemId); + uint32_t quality = 1; // white default if (const ItemQueryResponseData* info = getItemInfo(itemId)) { if (!info->name.empty()) itemName = info->name; + quality = info->quality; } std::string msg = "Received: " + itemName; if (count > 1) msg += " x" + std::to_string(count); addSystemChatMessage(msg); + if (itemLootCallback_) { + itemLootCallback_(itemId, count, quality, itemName); + } } LOG_INFO("Item push: itemId=", itemId, " count=", count, " showInChat=", static_cast(showInChat)); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index d7f9d772..4f78febf 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -359,6 +359,25 @@ void GameScreen::render(game::GameHandler& gameHandler) { pvpHonorCallbackSet_ = true; } + // Set up item loot toast callback (once) + if (!itemLootCallbackSet_) { + gameHandler.setItemLootCallback([this](uint32_t itemId, uint32_t count, + uint32_t quality, const std::string& name) { + // Coalesce: if same item already in queue, bump count and reset age + for (auto& t : itemLootToasts_) { + if (t.itemId == itemId) { + t.count += count; + t.age = 0.0f; + return; + } + } + if (itemLootToasts_.size() >= 5) + itemLootToasts_.erase(itemLootToasts_.begin()); + itemLootToasts_.push_back({itemId, count, quality, name, 0.0f}); + }); + itemLootCallbackSet_ = true; + } + // Set up UI error frame callback (once) if (!uiErrorCallbackSet_) { gameHandler.setUIErrorCallback([this](const std::string& msg) { @@ -692,6 +711,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderQuestProgressToasts(); renderPlayerLevelUpToasts(gameHandler); renderPvpHonorToasts(); + renderItemLootToasts(); renderZoneText(); // World map (M key toggle handled inside) @@ -18226,6 +18246,93 @@ void GameScreen::renderQuestProgressToasts() { } } +// --------------------------------------------------------------------------- +// Item loot toasts — quality-coloured strip at bottom-left when item received +// --------------------------------------------------------------------------- + +void GameScreen::renderItemLootToasts() { + if (itemLootToasts_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + for (auto& t : itemLootToasts_) t.age += dt; + itemLootToasts_.erase( + std::remove_if(itemLootToasts_.begin(), itemLootToasts_.end(), + [](const ItemLootToastEntry& t) { return t.age >= ITEM_LOOT_TOAST_DURATION; }), + itemLootToasts_.end()); + if (itemLootToasts_.empty()) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + // Quality colours (matching WoW convention) + static const ImU32 kQualityColors[] = { + IM_COL32(157, 157, 157, 255), // 0 grey (poor) + IM_COL32(255, 255, 255, 255), // 1 white (common) + IM_COL32( 30, 255, 30, 255), // 2 green (uncommon) + IM_COL32( 0, 112, 221, 255), // 3 blue (rare) + IM_COL32(163, 53, 238, 255), // 4 purple (epic) + IM_COL32(255, 128, 0, 255), // 5 orange (legendary) + }; + + // Stack at bottom-left above action bars; each item is 24 px tall + constexpr float TOAST_W = 260.0f; + constexpr float TOAST_H = 24.0f; + constexpr float TOAST_GAP = 2.0f; + constexpr float TOAST_X = 14.0f; + float baseY = screenH * 0.68f; // slightly above the whisper toasts + + ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); + const int count = static_cast(itemLootToasts_.size()); + + for (int i = 0; i < count; ++i) { + const auto& toast = itemLootToasts_[i]; + + float remaining = ITEM_LOOT_TOAST_DURATION - toast.age; + float alpha; + if (toast.age < 0.15f) + alpha = toast.age / 0.15f; + else if (remaining < 0.7f) + alpha = remaining / 0.7f; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + // Slide-in from left + float slideX = (toast.age < 0.15f) ? (TOAST_W * (1.0f - toast.age / 0.15f)) : 0.0f; + float tx = TOAST_X - slideX; + float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP); + + uint8_t bgA = static_cast(180 * alpha); + uint8_t fgA = static_cast(255 * alpha); + + // Background: very dark with quality-tinted left border accent + bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H), + IM_COL32(12, 12, 12, bgA), 3.0f); + + // Quality colour accent bar on left edge (3px wide) + ImU32 qualCol = kQualityColors[std::min(static_cast(5u), toast.quality)]; + ImU32 qualColA = (qualCol & 0x00FFFFFFu) | (static_cast(fgA) << 24u); + bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + 3.0f, ty + TOAST_H), qualColA, 3.0f); + + // "Loot:" label in dim white + bgDL->AddText(ImVec2(tx + 7.0f, ty + 5.0f), + IM_COL32(160, 160, 160, static_cast(200 * alpha)), "Loot:"); + + // Item name in quality colour + std::string displayName = toast.name.empty() ? ("Item #" + std::to_string(toast.itemId)) : toast.name; + if (displayName.size() > 26) { displayName.resize(23); displayName += "..."; } + bgDL->AddText(ImVec2(tx + 42.0f, ty + 5.0f), qualColA, displayName.c_str()); + + // Count (if > 1) + if (toast.count > 1) { + char countBuf[12]; + snprintf(countBuf, sizeof(countBuf), "x%u", toast.count); + bgDL->AddText(ImVec2(tx + TOAST_W - 34.0f, ty + 5.0f), + IM_COL32(200, 200, 200, static_cast(200 * alpha)), countBuf); + } + } +} + // --------------------------------------------------------------------------- // PvP honor credit toasts — shown at screen top-right on honorable kill // ---------------------------------------------------------------------------