From 20fef40b7bdb9175e6b42ae1a6699edfab470067 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 11:06:40 -0700 Subject: [PATCH] feat: show area trigger messages as screen banners SMSG_AREA_TRIGGER_MESSAGE events (dungeon enter messages, objective triggers, etc.) were previously only appended to chat. Now they also appear as animated slide-up toasts in the lower-center of the screen: blue-bordered dark panel with light-blue text, 4.5s lifetime with 35ms slide-in/out animation. Up to 4 simultaneous toasts stack vertically. Messages still go to chat as before. --- include/game/game_handler.hpp | 10 ++++++ include/ui/game_screen.hpp | 4 +++ src/game/game_handler.cpp | 5 ++- src/ui/game_screen.cpp | 67 +++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index e43d0a5e..17b66b75 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -540,6 +540,15 @@ public: const std::deque& getCombatLog() const { return combatLog_; } void clearCombatLog() { combatLog_.clear(); } + // Area trigger messages (SMSG_AREA_TRIGGER_MESSAGE) — drained by UI each frame + bool hasAreaTriggerMsg() const { return !areaTriggerMsgs_.empty(); } + std::string popAreaTriggerMsg() { + if (areaTriggerMsgs_.empty()) return {}; + std::string msg = areaTriggerMsgs_.front(); + areaTriggerMsgs_.pop_front(); + return msg; + } + // Threat struct ThreatEntry { uint64_t victimGuid = 0; @@ -2155,6 +2164,7 @@ private: std::vector combatText; static constexpr size_t MAX_COMBAT_LOG = 500; std::deque combatLog_; + std::deque areaTriggerMsgs_; // unitGuid → sorted threat list (descending by threat value) std::unordered_map> threatLists_; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 2b931b5f..e37fe6d7 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -119,6 +119,10 @@ private: // Zone entry toast: brief banner when entering a new zone struct ZoneToastEntry { std::string zoneName; float age = 0.0f; }; std::vector zoneToasts_; + + struct AreaTriggerToast { std::string text; float age = 0.0f; }; + std::vector areaTriggerToasts_; + void renderAreaTriggerToasts(float deltaTime, game::GameHandler& gameHandler); std::string lastKnownZone_; static constexpr float kZoneToastLifetime = 3.0f; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index c2fde19d..ccc620fc 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3877,7 +3877,10 @@ void GameHandler::handlePacket(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() >= 4) { /*uint32_t len =*/ packet.readUInt32(); std::string msg = packet.readString(); - if (!msg.empty()) addSystemChatMessage(msg); + if (!msg.empty()) { + addSystemChatMessage(msg); + areaTriggerMsgs_.push_back(msg); + } } break; } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 777f5705..87526741 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -562,6 +562,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderRepToasts(ImGui::GetIO().DeltaTime); renderQuestCompleteToasts(ImGui::GetIO().DeltaTime); renderZoneToasts(ImGui::GetIO().DeltaTime); + renderAreaTriggerToasts(ImGui::GetIO().DeltaTime, gameHandler); if (showRaidFrames_) { renderPartyFrames(gameHandler); } @@ -8572,6 +8573,72 @@ void GameScreen::renderZoneToasts(float deltaTime) { } } +// ─── Area Trigger Message Toasts ───────────────────────────────────────────── +void GameScreen::renderAreaTriggerToasts(float deltaTime, game::GameHandler& gameHandler) { + // Drain any pending messages from GameHandler + while (gameHandler.hasAreaTriggerMsg()) { + AreaTriggerToast t; + t.text = gameHandler.popAreaTriggerMsg(); + t.age = 0.0f; + areaTriggerToasts_.push_back(std::move(t)); + if (areaTriggerToasts_.size() > 4) + areaTriggerToasts_.erase(areaTriggerToasts_.begin()); + } + + // Age and prune + constexpr float kLifetime = 4.5f; + for (auto& t : areaTriggerToasts_) t.age += deltaTime; + areaTriggerToasts_.erase( + std::remove_if(areaTriggerToasts_.begin(), areaTriggerToasts_.end(), + [](const AreaTriggerToast& t) { return t.age >= kLifetime; }), + areaTriggerToasts_.end()); + if (areaTriggerToasts_.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; + + ImDrawList* draw = ImGui::GetForegroundDrawList(); + ImFont* font = ImGui::GetFont(); + constexpr float kSlideDur = 0.35f; + + for (int i = 0; i < static_cast(areaTriggerToasts_.size()); ++i) { + const auto& t = areaTriggerToasts_[i]; + + float slideIn = std::min(t.age, kSlideDur) / kSlideDur; + float slideOut = std::min(std::max(0.0f, kLifetime - t.age), kSlideDur) / kSlideDur; + float alpha = std::clamp(std::min(slideIn, slideOut), 0.0f, 1.0f); + + // Measure text + ImVec2 txtSz = font->CalcTextSizeA(13.0f, FLT_MAX, 0.0f, t.text.c_str()); + float toastW = txtSz.x + 30.0f; + float toastH = 30.0f; + + // Center horizontally, place below zone text (center of lower-third) + float toastX = (screenW - toastW) * 0.5f; + float toastY = screenH * 0.62f + i * (toastH + 3.0f); + // Slide up from below + float offY = (1.0f - std::min(slideIn, slideOut)) * (toastH + 12.0f); + toastY += offY; + + ImVec2 tl(toastX, toastY); + ImVec2 br(toastX + toastW, toastY + toastH); + + draw->AddRectFilled(tl, br, IM_COL32(8, 12, 22, (int)(alpha * 190)), 5.0f); + draw->AddRect(tl, br, IM_COL32(100, 160, 220, (int)(alpha * 200)), 5.0f, 0, 1.0f); + + float cx = tl.x + toastW * 0.5f; + // Shadow + draw->AddText(font, 13.0f, + ImVec2(cx - txtSz.x * 0.5f + 1, tl.y + (toastH - txtSz.y) * 0.5f + 1), + IM_COL32(0, 0, 0, (int)(alpha * 180)), t.text.c_str()); + // Text in light blue + draw->AddText(font, 13.0f, + ImVec2(cx - txtSz.x * 0.5f, tl.y + (toastH - txtSz.y) * 0.5f), + IM_COL32(180, 220, 255, (int)(alpha * 240)), t.text.c_str()); + } +} + // ============================================================ // Boss Encounter Frames // ============================================================