feat: show quest objective progress toasts on kill and item collection

Adds a visual progress overlay at bottom-right when quest kill counts
or item collection updates arrive. Each toast shows the quest title,
objective name, a fill-progress bar, and an X/Y count. Toasts coalesce
when the same objective updates multiple times, and auto-dismiss after 4s.
Wires a new QuestProgressCallback through GameHandler to trigger the UI.
This commit is contained in:
Kelsi 2026-03-12 15:57:09 -07:00
parent 5216582f15
commit c3afe543c6
4 changed files with 152 additions and 0 deletions

View file

@ -1432,6 +1432,13 @@ public:
// Area discovery callback — fires when SMSG_EXPLORATION_EXPERIENCE is received
using AreaDiscoveryCallback = std::function<void(const std::string& areaName, uint32_t xpGained)>;
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(const std::string& questTitle,
const std::string& objectiveName,
uint32_t current, uint32_t required)>;
void setQuestProgressCallback(QuestProgressCallback cb) { questProgressCallback_ = std::move(cb); }
const std::unordered_map<uint32_t, uint64_t>& 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_;

View file

@ -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: <ObjectiveName> 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<QuestProgressToastEntry> questToasts_;
bool questProgressCallbackSet_ = false;
void renderQuestProgressToasts();
// Zone discovery text ("Entering: <ZoneName>")
static constexpr float ZONE_TEXT_DURATION = 5.0f;
float zoneTextTimer_ = 0.0f;

View file

@ -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);
}

View file

@ -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<int>(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<uint8_t>(200 * alpha);
uint8_t fgA = static_cast<uint8_t>(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<uint8_t>(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<float>(toast.current) / static_cast<float>(toast.required))
: 1.0f;
// Bar background
bgDL->AddRectFilled(ImVec2(barX0, barY), ImVec2(barX1, barY + barH),
IM_COL32(50, 40, 10, static_cast<uint8_t>(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<uint8_t>(210 * alpha)), progBuf);
}
}
// ---------------------------------------------------------------------------
// Whisper toast notifications — brief overlay when a player whispers you
// ---------------------------------------------------------------------------