mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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:
parent
5216582f15
commit
c3afe543c6
4 changed files with 152 additions and 0 deletions
|
|
@ -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_;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue