diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index a02077f1..047220b5 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1432,6 +1432,13 @@ public: // Area discovery callback — fires when SMSG_EXPLORATION_EXPERIENCE is received using AreaDiscoveryCallback = std::function; void setAreaDiscoveryCallback(AreaDiscoveryCallback cb) { areaDiscoveryCallback_ = std::move(cb); } + + // Quest objective progress callback — fires on SMSG_QUESTUPDATE_ADD_KILL / ADD_ITEM + // questTitle: name of the quest; objectiveName: creature/item name; current/required counts + using QuestProgressCallback = std::function; + void setQuestProgressCallback(QuestProgressCallback cb) { questProgressCallback_ = std::move(cb); } const std::unordered_map& getCriteriaProgress() const { return criteriaProgress_; } /// Returns the WoW PackedTime earn date for an achievement, or 0 if unknown. uint32_t getAchievementDate(uint32_t id) const { @@ -2754,6 +2761,7 @@ private: OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_; AchievementEarnedCallback achievementEarnedCallback_; AreaDiscoveryCallback areaDiscoveryCallback_; + QuestProgressCallback questProgressCallback_; MountCallback mountCallback_; TaxiPrecacheCallback taxiPrecacheCallback_; TaxiOrientationCallback taxiOrientationCallback_; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 8861be60..29afe02b 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -538,6 +538,19 @@ private: size_t whisperSeenCount_ = 0; // how many chat entries have been scanned for whispers void renderWhisperToasts(); + // Quest objective progress toast ("Quest: X/Y") + struct QuestProgressToastEntry { + std::string questTitle; + std::string objectiveName; + uint32_t current = 0; + uint32_t required = 0; + float age = 0.0f; + }; + static constexpr float QUEST_TOAST_DURATION = 4.0f; + std::vector questToasts_; + bool questProgressCallbackSet_ = false; + void renderQuestProgressToasts(); + // 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 0ff47db3..8f433ebd 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -4484,6 +4484,10 @@ void GameHandler::handlePacket(network::Packet& packet) { progressMsg += std::to_string(count) + "/" + std::to_string(reqCount); addSystemChatMessage(progressMsg); + if (questProgressCallback_) { + questProgressCallback_(quest.title, creatureName, count, reqCount); + } + LOG_INFO("Updated kill count for quest ", questId, ": ", count, "/", reqCount); break; @@ -4538,6 +4542,26 @@ void GameHandler::handlePacket(network::Packet& packet) { updatedAny = true; } addSystemChatMessage("Quest item: " + itemLabel + " (" + std::to_string(count) + ")"); + + if (questProgressCallback_ && updatedAny) { + // Find the quest that tracks this item to get title and required count + for (const auto& quest : questLog_) { + if (quest.complete) continue; + if (quest.itemCounts.count(itemId) == 0) continue; + uint32_t required = 0; + auto rIt = quest.requiredItemCounts.find(itemId); + if (rIt != quest.requiredItemCounts.end()) required = rIt->second; + if (required == 0) { + for (const auto& obj : quest.itemObjectives) { + if (obj.itemId == itemId) { required = obj.required; break; } + } + } + if (required == 0) required = count; + questProgressCallback_(quest.title, itemLabel, count, required); + break; + } + } + LOG_INFO("Quest item update: itemId=", itemId, " count=", count, " trackedQuestsUpdated=", updatedAny); } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index c6454449..112665e7 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -310,6 +310,26 @@ void GameScreen::render(game::GameHandler& gameHandler) { areaDiscoveryCallbackSet_ = true; } + // Set up quest objective progress toast callback (once) + if (!questProgressCallbackSet_) { + gameHandler.setQuestProgressCallback([this](const std::string& questTitle, + const std::string& objectiveName, + uint32_t current, uint32_t required) { + // Coalesce: if the same objective already has a toast, just update counts + for (auto& t : questToasts_) { + if (t.questTitle == questTitle && t.objectiveName == objectiveName) { + t.current = current; + t.required = required; + t.age = 0.0f; // restart lifetime + return; + } + } + if (questToasts_.size() >= 4) questToasts_.erase(questToasts_.begin()); + questToasts_.push_back({questTitle, objectiveName, current, required, 0.0f}); + }); + questProgressCallbackSet_ = true; + } + // Set up UI error frame callback (once) if (!uiErrorCallbackSet_) { gameHandler.setUIErrorCallback([this](const std::string& msg) { @@ -640,6 +660,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderAchievementToast(); renderDiscoveryToast(); renderWhisperToasts(); + renderQuestProgressToasts(); renderZoneText(); // World map (M key toggle handled inside) @@ -18038,6 +18059,92 @@ void GameScreen::renderDiscoveryToast() { } } +// --------------------------------------------------------------------------- +// Quest objective progress toasts — shown at screen bottom-right on kill/item updates +// --------------------------------------------------------------------------- + +void GameScreen::renderQuestProgressToasts() { + if (questToasts_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + for (auto& t : questToasts_) t.age += dt; + questToasts_.erase( + std::remove_if(questToasts_.begin(), questToasts_.end(), + [](const QuestProgressToastEntry& t) { return t.age >= QUEST_TOAST_DURATION; }), + questToasts_.end()); + if (questToasts_.empty()) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + // Stack at bottom-right, just above action bar area + constexpr float TOAST_W = 240.0f; + constexpr float TOAST_H = 48.0f; + constexpr float TOAST_GAP = 4.0f; + float baseY = screenH * 0.72f; + float toastX = screenW - TOAST_W - 14.0f; + + ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); + const int count = static_cast(questToasts_.size()); + + for (int i = 0; i < count; ++i) { + const auto& toast = questToasts_[i]; + + float remaining = QUEST_TOAST_DURATION - toast.age; + float alpha; + if (toast.age < 0.2f) + alpha = toast.age / 0.2f; + else if (remaining < 1.0f) + alpha = remaining; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP); + + uint8_t bgA = static_cast(200 * alpha); + uint8_t fgA = static_cast(255 * alpha); + + // Background: dark amber tint (quest color convention) + bgDL->AddRectFilled(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(35, 25, 5, bgA), 5.0f); + bgDL->AddRect(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(200, 160, 30, static_cast(160 * alpha)), 5.0f, 0, 1.5f); + + // Quest title (gold, small) + bgDL->AddText(ImVec2(toastX + 8.0f, ty + 5.0f), + IM_COL32(220, 180, 50, fgA), toast.questTitle.c_str()); + + // Progress bar + text: "ObjectiveName X / Y" + float barY = ty + 21.0f; + float barX0 = toastX + 8.0f; + float barX1 = toastX + TOAST_W - 8.0f; + float barH = 8.0f; + float pct = (toast.required > 0) + ? std::min(1.0f, static_cast(toast.current) / static_cast(toast.required)) + : 1.0f; + // Bar background + bgDL->AddRectFilled(ImVec2(barX0, barY), ImVec2(barX1, barY + barH), + IM_COL32(50, 40, 10, static_cast(180 * alpha)), 3.0f); + // Bar fill — green when complete, amber otherwise + ImU32 barCol = (pct >= 1.0f) ? IM_COL32(60, 220, 80, fgA) : IM_COL32(200, 160, 30, fgA); + bgDL->AddRectFilled(ImVec2(barX0, barY), + ImVec2(barX0 + (barX1 - barX0) * pct, barY + barH), + barCol, 3.0f); + + // Objective name + count + char progBuf[48]; + if (!toast.objectiveName.empty()) + snprintf(progBuf, sizeof(progBuf), "%.22s: %u/%u", + toast.objectiveName.c_str(), toast.current, toast.required); + else + snprintf(progBuf, sizeof(progBuf), "%u/%u", toast.current, toast.required); + bgDL->AddText(ImVec2(toastX + 8.0f, ty + 32.0f), + IM_COL32(220, 220, 200, static_cast(210 * alpha)), progBuf); + } +} + // --------------------------------------------------------------------------- // Whisper toast notifications — brief overlay when a player whispers you // ---------------------------------------------------------------------------