From b34bf397469347d7bbc8a6cdf175c2b9e85d1654 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 04:53:03 -0700 Subject: [PATCH] feat: add quest completion toast notification When a quest is turned in (SMSG_QUESTGIVER_QUEST_COMPLETE), a gold-bordered toast slides in from the right showing "Quest Complete" header with the quest title, consistent with the rep change and achievement toast systems. --- include/game/game_handler.hpp | 7 ++++ include/ui/game_screen.hpp | 7 ++++ src/game/game_handler.cpp | 4 ++ src/ui/game_screen.cpp | 72 +++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 79460a17..72bcdd54 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1330,6 +1330,10 @@ public: using RepChangeCallback = std::function; void setRepChangeCallback(RepChangeCallback cb) { repChangeCallback_ = std::move(cb); } + // Quest turn-in completion callback + using QuestCompleteCallback = std::function; + void setQuestCompleteCallback(QuestCompleteCallback cb) { questCompleteCallback_ = std::move(cb); } + // Mount state using MountCallback = std::function; // 0 = dismount void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); } @@ -2630,6 +2634,9 @@ private: // ---- Reputation change callback ---- RepChangeCallback repChangeCallback_; + + // ---- Quest completion callback ---- + QuestCompleteCallback questCompleteCallback_; }; } // namespace game diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 709dc502..4ae2a668 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -100,6 +100,12 @@ private: std::vector repToasts_; bool repChangeCallbackSet_ = false; static constexpr float kRepToastLifetime = 3.5f; + + // Quest completion toast: slide-in when a quest is turned in + struct QuestCompleteToastEntry { uint32_t questId = 0; std::string title; float age = 0.0f; }; + std::vector questCompleteToasts_; + bool questCompleteCallbackSet_ = false; + static constexpr float kQuestCompleteToastLifetime = 4.0f; bool showPlayerInfo = false; bool showSocialFrame_ = false; // O key toggles social/friends list bool showGuildRoster_ = false; @@ -283,6 +289,7 @@ private: void renderBossFrames(game::GameHandler& gameHandler); void renderUIErrors(game::GameHandler& gameHandler, float deltaTime); void renderRepToasts(float deltaTime); + void renderQuestCompleteToasts(float deltaTime); void renderGroupInvitePopup(game::GameHandler& gameHandler); void renderDuelRequestPopup(game::GameHandler& gameHandler); void renderLootRollPopup(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index b3e57ccc..1d525ecb 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -4381,6 +4381,10 @@ void GameHandler::handlePacket(network::Packet& packet) { } for (auto it = questLog_.begin(); it != questLog_.end(); ++it) { if (it->questId == questId) { + // Fire toast callback before erasing + if (questCompleteCallback_) { + questCompleteCallback_(questId, it->title); + } questLog_.erase(it); LOG_INFO(" Removed quest ", questId, " from quest log"); break; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 0f0451ae..692365c0 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -245,6 +245,15 @@ void GameScreen::render(game::GameHandler& gameHandler) { repChangeCallbackSet_ = true; } + // Set up quest completion toast callback (once) + if (!questCompleteCallbackSet_) { + gameHandler.setQuestCompleteCallback([this](uint32_t id, const std::string& title) { + questCompleteToasts_.push_back({id, title, 0.0f}); + if (questCompleteToasts_.size() > 3) questCompleteToasts_.erase(questCompleteToasts_.begin()); + }); + questCompleteCallbackSet_ = true; + } + // Apply UI transparency setting float prevAlpha = ImGui::GetStyle().Alpha; ImGui::GetStyle().Alpha = uiOpacity_; @@ -465,6 +474,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderDPSMeter(gameHandler); renderUIErrors(gameHandler, ImGui::GetIO().DeltaTime); renderRepToasts(ImGui::GetIO().DeltaTime); + renderQuestCompleteToasts(ImGui::GetIO().DeltaTime); if (showRaidFrames_) { renderPartyFrames(gameHandler); } @@ -7128,6 +7138,68 @@ void GameScreen::renderRepToasts(float deltaTime) { } } +void GameScreen::renderQuestCompleteToasts(float deltaTime) { + for (auto& e : questCompleteToasts_) e.age += deltaTime; + questCompleteToasts_.erase( + std::remove_if(questCompleteToasts_.begin(), questCompleteToasts_.end(), + [](const QuestCompleteToastEntry& e) { return e.age >= kQuestCompleteToastLifetime; }), + questCompleteToasts_.end()); + + if (questCompleteToasts_.empty()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + const float toastW = 260.0f; + const float toastH = 40.0f; + const float padY = 4.0f; + const float baseY = screenH - 220.0f; // above rep toasts + + ImDrawList* draw = ImGui::GetForegroundDrawList(); + ImFont* font = ImGui::GetFont(); + float fontSize = ImGui::GetFontSize(); + + for (int i = 0; i < static_cast(questCompleteToasts_.size()); ++i) { + const auto& e = questCompleteToasts_[i]; + constexpr float kSlideDur = 0.3f; + float slideIn = std::min(e.age, kSlideDur) / kSlideDur; + float slideOut = std::min(std::max(0.0f, kQuestCompleteToastLifetime - e.age), kSlideDur) / kSlideDur; + float slide = std::min(slideIn, slideOut); + float alpha = std::clamp(slide, 0.0f, 1.0f); + + float xFull = screenW - 14.0f - toastW; + float xStart = screenW + 10.0f; + float toastX = xStart + (xFull - xStart) * slide; + float toastY = baseY - i * (toastH + padY); + + ImVec2 tl(toastX, toastY); + ImVec2 br(toastX + toastW, toastY + toastH); + + // Background + gold border (quest completion) + draw->AddRectFilled(tl, br, IM_COL32(20, 18, 8, (int)(alpha * 210)), 5.0f); + draw->AddRect(tl, br, IM_COL32(220, 180, 30, (int)(alpha * 230)), 5.0f, 0, 1.5f); + + // Scroll icon placeholder (gold diamond) + float iconCx = tl.x + 18.0f; + float iconCy = tl.y + toastH * 0.5f; + draw->AddCircleFilled(ImVec2(iconCx, iconCy), 7.0f, IM_COL32(210, 170, 20, (int)(alpha * 230))); + draw->AddCircle (ImVec2(iconCx, iconCy), 7.0f, IM_COL32(255, 220, 50, (int)(alpha * 200))); + + // "Quest Complete" header in gold + const char* header = "Quest Complete"; + draw->AddText(font, fontSize * 0.78f, + ImVec2(tl.x + 34.0f, tl.y + 4.0f), + IM_COL32(240, 200, 40, (int)(alpha * 240)), header); + + // Quest title in off-white + const char* titleStr = e.title.empty() ? "Unknown Quest" : e.title.c_str(); + draw->AddText(font, fontSize * 0.82f, + ImVec2(tl.x + 34.0f, tl.y + toastH * 0.5f + 1.0f), + IM_COL32(220, 215, 195, (int)(alpha * 220)), titleStr); + } +} + // ============================================================ // Boss Encounter Frames // ============================================================