diff --git a/CMakeLists.txt b/CMakeLists.txt index 06a4727e..da19c235 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -557,6 +557,7 @@ set(WOWEE_SOURCES src/ui/character_screen.cpp src/ui/game_screen.cpp src/ui/chat_panel.cpp + src/ui/toast_manager.cpp src/ui/inventory_screen.cpp src/ui/quest_log_screen.cpp src/ui/spellbook_screen.cpp diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index b6dd4d27..2260fb97 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -10,6 +10,7 @@ #include "ui/talent_screen.hpp" #include "ui/keybinding_manager.hpp" #include "ui/chat_panel.hpp" +#include "ui/toast_manager.hpp" #include #include #include @@ -48,6 +49,9 @@ private: // Chat panel (extracted from GameScreen — owns all chat state and rendering) ChatPanel chatPanel_; + // Toast manager (extracted from GameScreen — owns all toast/notification state and rendering) + ToastManager toastManager_; + // Action bar error-flash: spellId → wall-clock time (seconds) when the flash ends. // Populated by the SpellCastFailedCallback; queried during action bar button rendering. std::unordered_map actionFlashEndTimes_; @@ -65,8 +69,7 @@ private: float damageFlashAlpha_ = 0.0f; // Screen edge flash intensity (fades to 0) bool damageFlashEnabled_ = true; bool lowHealthVignetteEnabled_ = true; // Persistent pulsing red vignette below 20% HP - float levelUpFlashAlpha_ = 0.0f; // Golden level-up burst effect (fades to 0) - uint32_t levelUpDisplayLevel_ = 0; // Level shown in level-up text + // Raid Warning / Boss Emote big-text overlay (center-screen, fades after 5s) struct RaidWarnEntry { @@ -87,34 +90,14 @@ private: bool castFailedCallbackSet_ = false; static constexpr float kActionFlashDuration = 0.5f; // seconds for error-red overlay to fade - // Reputation change toast: brief colored slide-in below minimap - struct RepToastEntry { std::string factionName; int32_t delta = 0; int32_t standing = 0; float age = 0.0f; }; - 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; - - // 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; // Death screen: elapsed time since the death dialog first appeared float deathElapsed_ = 0.0f; bool deathTimerRunning_ = false; // WoW forces release after ~6 minutes; show countdown until then static constexpr float kForcedReleaseSec = 360.0f; - void renderZoneToasts(float deltaTime); + bool showPlayerInfo = false; bool showSocialFrame_ = false; // O key toggles social/friends list bool showGuildRoster_ = false; @@ -291,8 +274,7 @@ private: void renderPartyFrames(game::GameHandler& gameHandler); 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 renderDuelCountdown(game::GameHandler& gameHandler); @@ -465,8 +447,7 @@ private: static std::string getSettingsPath(); - bool levelUpCallbackSet_ = false; - bool achievementCallbackSet_ = false; + // Mail compose state char mailRecipientBuffer_[256] = ""; @@ -520,107 +501,13 @@ private: glm::vec2 leftClickPressPos_ = glm::vec2(0.0f); bool leftClickWasPress_ = false; - // Level-up ding animation - static constexpr float DING_DURATION = 4.0f; - float dingTimer_ = 0.0f; - uint32_t dingLevel_ = 0; - uint32_t dingHpDelta_ = 0; - uint32_t dingManaDelta_ = 0; - uint32_t dingStats_[5] = {}; // str/agi/sta/int/spi deltas - void renderDingEffect(); - // Achievement toast banner - static constexpr float ACHIEVEMENT_TOAST_DURATION = 5.0f; - float achievementToastTimer_ = 0.0f; - uint32_t achievementToastId_ = 0; - std::string achievementToastName_; - void renderAchievementToast(); - - // Area discovery toast ("Discovered! +XP XP") - static constexpr float DISCOVERY_TOAST_DURATION = 4.0f; - float discoveryToastTimer_ = 0.0f; - std::string discoveryToastName_; - uint32_t discoveryToastXP_ = 0; - bool areaDiscoveryCallbackSet_ = false; - void renderDiscoveryToast(); - - // Whisper toast — brief overlay at screen top when a whisper arrives while chat is not focused - struct WhisperToastEntry { - std::string sender; - std::string preview; // first ~60 chars of message - float age = 0.0f; - }; - static constexpr float WHISPER_TOAST_DURATION = 5.0f; - std::vector whisperToasts_; - 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(); - - // Nearby player level-up toast (" is now level X!") - struct PlayerLevelUpToastEntry { - uint64_t guid = 0; - std::string playerName; // resolved lazily at render time - uint32_t newLevel = 0; - float age = 0.0f; - }; - static constexpr float PLAYER_LEVELUP_TOAST_DURATION = 4.0f; - std::vector playerLevelUpToasts_; - bool otherPlayerLevelUpCallbackSet_ = false; - void renderPlayerLevelUpToasts(game::GameHandler& gameHandler); - - // PvP honor credit toast ("+N Honor" shown when an honorable kill is credited) - struct PvpHonorToastEntry { - uint32_t honor = 0; - uint32_t victimRank = 0; // 0 = unranked / not available - float age = 0.0f; - }; - static constexpr float PVP_HONOR_TOAST_DURATION = 3.5f; - std::vector pvpHonorToasts_; - bool pvpHonorCallbackSet_ = false; - void renderPvpHonorToasts(); - - // Item loot toast — quality-coloured popup when an item is received - struct ItemLootToastEntry { - uint32_t itemId = 0; - uint32_t count = 0; - uint32_t quality = 1; // 0=grey,1=white,2=green,3=blue,4=purple,5=orange - std::string name; - float age = 0.0f; - }; - static constexpr float ITEM_LOOT_TOAST_DURATION = 3.0f; - std::vector itemLootToasts_; - bool itemLootCallbackSet_ = false; - void renderItemLootToasts(); - - // Resurrection flash: brief "You have been resurrected!" overlay on ghost→alive transition - float resurrectFlashTimer_ = 0.0f; - static constexpr float kResurrectFlashDuration = 3.0f; - bool ghostStateCallbackSet_ = false; bool appearanceCallbackSet_ = false; bool ghostOpacityStateKnown_ = false; bool ghostOpacityLastState_ = false; uint32_t ghostOpacityLastInstanceId_ = 0; - void renderResurrectFlash(); - // Zone discovery text ("Entering: ") - static constexpr float ZONE_TEXT_DURATION = 5.0f; - float zoneTextTimer_ = 0.0f; - std::string zoneTextName_; - std::string lastKnownZoneName_; - uint32_t lastKnownWorldStateZoneId_ = 0; - void renderZoneText(game::GameHandler& gameHandler); + void renderWeatherOverlay(game::GameHandler& gameHandler); // Cooldown tracker @@ -635,11 +522,8 @@ private: size_t dpsLogSeenCount_ = 0; // log entries already scanned public: - void triggerDing(uint32_t newLevel, uint32_t hpDelta = 0, uint32_t manaDelta = 0, - uint32_t str = 0, uint32_t agi = 0, uint32_t sta = 0, - uint32_t intel = 0, uint32_t spi = 0); - void triggerAchievementToast(uint32_t achievementId, std::string name = {}); void openDungeonFinder() { showDungeonFinder_ = true; } + ToastManager& toastManager() { return toastManager_; } }; } // namespace ui diff --git a/include/ui/toast_manager.hpp b/include/ui/toast_manager.hpp new file mode 100644 index 00000000..29c27983 --- /dev/null +++ b/include/ui/toast_manager.hpp @@ -0,0 +1,190 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace wowee { +namespace game { class GameHandler; } +namespace ui { + +/** + * Toast / notification overlay manager + * + * Owns all toast state, callbacks, and rendering: + * level-up ding, achievement, area discovery, whisper, quest progress, + * player level-up, PvP honor, item loot, reputation, quest complete, + * zone entry, area trigger, resurrect flash, and zone text. + */ +class ToastManager { +public: + ToastManager() = default; + + /// Register toast-related callbacks on GameHandler (idempotent — safe every frame) + void setupCallbacks(game::GameHandler& gameHandler); + + /// Render "early" toasts (rep, quest-complete, zone, area-trigger) — called before action bars + void renderEarlyToasts(float deltaTime, game::GameHandler& gameHandler); + + /// Render "late" toasts (ding, achievement, discovery, whisper, quest progress, + /// player level-up, PvP honor, item loot, resurrect flash, zone text) — called after escape menu + void renderLateToasts(game::GameHandler& gameHandler); + + /// Fire level-up ding animation + sound + void triggerDing(uint32_t newLevel, uint32_t hpDelta = 0, uint32_t manaDelta = 0, + uint32_t str = 0, uint32_t agi = 0, uint32_t sta = 0, + uint32_t intel = 0, uint32_t spi = 0); + + /// Fire achievement earned toast + sound + void triggerAchievementToast(uint32_t achievementId, std::string name = {}); + + // --- public state consumed by GameScreen for the golden burst overlay --- + float levelUpFlashAlpha = 0.0f; + uint32_t levelUpDisplayLevel = 0; + +private: + // ---- Ding effect (own level-up) ---- + static constexpr float DING_DURATION = 4.0f; + float dingTimer_ = 0.0f; + uint32_t dingLevel_ = 0; + uint32_t dingHpDelta_ = 0; + uint32_t dingManaDelta_ = 0; + uint32_t dingStats_[5] = {}; + void renderDingEffect(); + + // ---- Achievement toast ---- + static constexpr float ACHIEVEMENT_TOAST_DURATION = 5.0f; + float achievementToastTimer_ = 0.0f; + uint32_t achievementToastId_ = 0; + std::string achievementToastName_; + bool achievementCallbackSet_ = false; + void renderAchievementToast(); + + // ---- Area discovery toast ---- + static constexpr float DISCOVERY_TOAST_DURATION = 4.0f; + float discoveryToastTimer_ = 0.0f; + std::string discoveryToastName_; + uint32_t discoveryToastXP_ = 0; + bool areaDiscoveryCallbackSet_ = false; + void renderDiscoveryToast(); + + // ---- Whisper toast ---- + struct WhisperToastEntry { + std::string sender; + std::string preview; + float age = 0.0f; + }; + static constexpr float WHISPER_TOAST_DURATION = 5.0f; + std::vector whisperToasts_; + size_t whisperSeenCount_ = 0; + void renderWhisperToasts(); + + // ---- Quest objective progress toast ---- + 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(); + + // ---- Nearby player level-up toast ---- + struct PlayerLevelUpToastEntry { + uint64_t guid = 0; + std::string playerName; + uint32_t newLevel = 0; + float age = 0.0f; + }; + static constexpr float PLAYER_LEVELUP_TOAST_DURATION = 4.0f; + std::vector playerLevelUpToasts_; + bool otherPlayerLevelUpCallbackSet_ = false; + void renderPlayerLevelUpToasts(game::GameHandler& gameHandler); + + // ---- PvP honor toast ---- + struct PvpHonorToastEntry { + uint32_t honor = 0; + uint32_t victimRank = 0; + float age = 0.0f; + }; + static constexpr float PVP_HONOR_TOAST_DURATION = 3.5f; + std::vector pvpHonorToasts_; + bool pvpHonorCallbackSet_ = false; + void renderPvpHonorToasts(); + + // ---- Item loot toast ---- + struct ItemLootToastEntry { + uint32_t itemId = 0; + uint32_t count = 0; + uint32_t quality = 1; + std::string name; + float age = 0.0f; + }; + static constexpr float ITEM_LOOT_TOAST_DURATION = 3.0f; + std::vector itemLootToasts_; + bool itemLootCallbackSet_ = false; + void renderItemLootToasts(); + + // ---- Reputation change toast ---- + struct RepToastEntry { + std::string factionName; + int32_t delta = 0; + int32_t standing = 0; + float age = 0.0f; + }; + std::vector repToasts_; + bool repChangeCallbackSet_ = false; + static constexpr float kRepToastLifetime = 3.5f; + void renderRepToasts(float deltaTime); + + // ---- Quest completion toast ---- + 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; + void renderQuestCompleteToasts(float deltaTime); + + // ---- Zone entry toast ---- + struct ZoneToastEntry { + std::string zoneName; + float age = 0.0f; + }; + std::vector zoneToasts_; + std::string lastKnownZone_; + static constexpr float kZoneToastLifetime = 3.0f; + void renderZoneToasts(float deltaTime); + + // ---- Area trigger message toast ---- + struct AreaTriggerToast { + std::string text; + float age = 0.0f; + }; + std::vector areaTriggerToasts_; + void renderAreaTriggerToasts(float deltaTime, game::GameHandler& gameHandler); + + // ---- Resurrection flash ---- + float resurrectFlashTimer_ = 0.0f; + static constexpr float kResurrectFlashDuration = 3.0f; + bool ghostStateCallbackSet_ = false; + void renderResurrectFlash(); + + // ---- Zone discovery text ("Entering: ") ---- + static constexpr float ZONE_TEXT_DURATION = 5.0f; + float zoneTextTimer_ = 0.0f; + std::string zoneTextName_; + std::string lastKnownZoneName_; + uint32_t lastKnownWorldStateZoneId_ = 0; + void renderZoneText(game::GameHandler& gameHandler); +}; + +} // namespace ui +} // namespace wowee diff --git a/src/core/application.cpp b/src/core/application.cpp index e4ee3342..443276f5 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -2928,7 +2928,7 @@ void Application::setupUICallbacks() { // Level-up callback — play sound, cheer emote, and trigger UI ding overlay + 3D effect gameHandler->setLevelUpCallback([this](uint32_t newLevel) { if (uiManager) { - uiManager->getGameScreen().triggerDing(newLevel); + uiManager->getGameScreen().toastManager().triggerDing(newLevel); } if (renderer) { renderer->triggerLevelUpEffect(renderer->getCharacterPosition()); @@ -2938,7 +2938,7 @@ void Application::setupUICallbacks() { // Achievement earned callback — show toast banner gameHandler->setAchievementEarnedCallback([this](uint32_t achievementId, const std::string& name) { if (uiManager) { - uiManager->getGameScreen().triggerAchievementToast(achievementId, name); + uiManager->getGameScreen().toastManager().triggerAchievementToast(achievementId, name); } }); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 0349ab34..abead7ec 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -252,114 +252,7 @@ GameScreen::GameScreen() { void GameScreen::render(game::GameHandler& gameHandler) { // Set up chat bubble callback (once) and cache game handler in ChatPanel chatPanel_.setupCallbacks(gameHandler); - - // Set up level-up callback (once) - if (!levelUpCallbackSet_) { - gameHandler.setLevelUpCallback([this, &gameHandler](uint32_t newLevel) { - levelUpFlashAlpha_ = 1.0f; - levelUpDisplayLevel_ = newLevel; - const auto& d = gameHandler.getLastLevelUpDeltas(); - triggerDing(newLevel, d.hp, d.mana, d.str, d.agi, d.sta, d.intel, d.spi); - }); - levelUpCallbackSet_ = true; - } - - // Set up achievement toast callback (once) - if (!achievementCallbackSet_) { - gameHandler.setAchievementEarnedCallback([this](uint32_t id, const std::string& name) { - triggerAchievementToast(id, name); - }); - achievementCallbackSet_ = true; - } - - // Set up area discovery toast callback (once) - if (!areaDiscoveryCallbackSet_) { - gameHandler.setAreaDiscoveryCallback([this](const std::string& areaName, uint32_t xpGained) { - discoveryToastName_ = areaName.empty() ? "New Area" : areaName; - discoveryToastXP_ = xpGained; - discoveryToastTimer_ = DISCOVERY_TOAST_DURATION; - }); - 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 other-player level-up toast callback (once) - if (!otherPlayerLevelUpCallbackSet_) { - gameHandler.setOtherPlayerLevelUpCallback([this](uint64_t guid, uint32_t newLevel) { - // Coalesce: update existing toast for same player - for (auto& t : playerLevelUpToasts_) { - if (t.guid == guid) { - t.newLevel = newLevel; - t.age = 0.0f; - return; - } - } - if (playerLevelUpToasts_.size() >= 3) - playerLevelUpToasts_.erase(playerLevelUpToasts_.begin()); - playerLevelUpToasts_.push_back({guid, "", newLevel, 0.0f}); - }); - otherPlayerLevelUpCallbackSet_ = true; - } - - // Set up PvP honor credit toast callback (once) - if (!pvpHonorCallbackSet_) { - gameHandler.setPvpHonorCallback([this](uint32_t honor, uint64_t /*victimGuid*/, uint32_t rank) { - if (honor == 0) return; - pvpHonorToasts_.push_back({honor, rank, 0.0f}); - if (pvpHonorToasts_.size() > 4) - pvpHonorToasts_.erase(pvpHonorToasts_.begin()); - }); - pvpHonorCallbackSet_ = true; - } - - // Set up item loot toast callback (once) - if (!itemLootCallbackSet_) { - gameHandler.setItemLootCallback([this](uint32_t itemId, uint32_t count, - uint32_t quality, const std::string& name) { - // Coalesce: if same item already in queue, bump count and reset age - for (auto& t : itemLootToasts_) { - if (t.itemId == itemId) { - t.count += count; - t.age = 0.0f; - return; - } - } - if (itemLootToasts_.size() >= 5) - itemLootToasts_.erase(itemLootToasts_.begin()); - itemLootToasts_.push_back({itemId, count, quality, name, 0.0f}); - }); - itemLootCallbackSet_ = true; - } - - // Set up ghost-state callback to flash "You have been resurrected!" on revival (once) - if (!ghostStateCallbackSet_) { - gameHandler.setGhostStateCallback([this](bool isGhost) { - if (!isGhost) { - // Transitioning ghost→alive: trigger the resurrection flash - resurrectFlashTimer_ = kResurrectFlashDuration; - } - }); - ghostStateCallbackSet_ = true; - } + toastManager_.setupCallbacks(gameHandler); // Set up appearance-changed callback to refresh inventory preview (barber shop, etc.) if (!appearanceCallbackSet_) { @@ -392,24 +285,6 @@ void GameScreen::render(game::GameHandler& gameHandler) { castFailedCallbackSet_ = true; } - // Set up reputation change toast callback (once) - if (!repChangeCallbackSet_) { - gameHandler.setRepChangeCallback([this](const std::string& name, int32_t delta, int32_t standing) { - repToasts_.push_back({name, delta, standing, 0.0f}); - if (repToasts_.size() > 4) repToasts_.erase(repToasts_.begin()); - }); - 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_; @@ -543,20 +418,6 @@ void GameScreen::render(game::GameHandler& gameHandler) { gameHandler.setAutoSellGrey(pendingAutoSellGrey); gameHandler.setAutoRepair(pendingAutoRepair); - // Zone entry detection — fire a toast when the renderer's zone name changes - if (auto* rend = core::Application::getInstance().getRenderer()) { - const std::string& curZone = rend->getCurrentZoneName(); - if (!curZone.empty() && curZone != lastKnownZone_) { - if (!lastKnownZone_.empty()) { - // Genuine zone change (not first entry) - zoneToasts_.push_back({curZone, 0.0f}); - if (zoneToasts_.size() > 3) - zoneToasts_.erase(zoneToasts_.begin()); - } - lastKnownZone_ = curZone; - } - } - // Sync chat auto-join settings to GameHandler gameHandler.chatAutoJoin.general = chatPanel_.chatAutoJoinGeneral; gameHandler.chatAutoJoin.trade = chatPanel_.chatAutoJoinTrade; @@ -638,10 +499,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderDPSMeter(gameHandler); renderDurabilityWarning(gameHandler); renderUIErrors(gameHandler, ImGui::GetIO().DeltaTime); - renderRepToasts(ImGui::GetIO().DeltaTime); - renderQuestCompleteToasts(ImGui::GetIO().DeltaTime); - renderZoneToasts(ImGui::GetIO().DeltaTime); - renderAreaTriggerToasts(ImGui::GetIO().DeltaTime, gameHandler); + toastManager_.renderEarlyToasts(ImGui::GetIO().DeltaTime, gameHandler); if (showRaidFrames_) { renderPartyFrames(gameHandler); } @@ -704,16 +562,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { chatPanel_.renderBubbles(gameHandler); renderEscapeMenu(); renderSettingsWindow(); - renderDingEffect(); - renderAchievementToast(); - renderDiscoveryToast(); - renderWhisperToasts(); - renderQuestProgressToasts(); - renderPlayerLevelUpToasts(gameHandler); - renderPvpHonorToasts(); - renderItemLootToasts(); - renderResurrectFlash(); - renderZoneText(gameHandler); + toastManager_.renderLateToasts(gameHandler); renderWeatherOverlay(gameHandler); // World map (M key toggle handled inside) @@ -989,15 +838,15 @@ void GameScreen::render(game::GameHandler& gameHandler) { } // Level-up golden burst overlay - if (levelUpFlashAlpha_ > 0.0f) { - levelUpFlashAlpha_ -= ImGui::GetIO().DeltaTime * 1.0f; // fade over ~1 second - if (levelUpFlashAlpha_ < 0.0f) levelUpFlashAlpha_ = 0.0f; + if (toastManager_.levelUpFlashAlpha > 0.0f) { + toastManager_.levelUpFlashAlpha -= ImGui::GetIO().DeltaTime * 1.0f; // fade over ~1 second + if (toastManager_.levelUpFlashAlpha < 0.0f) toastManager_.levelUpFlashAlpha = 0.0f; ImDrawList* fg = ImGui::GetForegroundDrawList(); ImGuiIO& io = ImGui::GetIO(); const float W = io.DisplaySize.x; const float H = io.DisplaySize.y; - const int alpha = static_cast(levelUpFlashAlpha_ * 160.0f); + const int alpha = static_cast(toastManager_.levelUpFlashAlpha * 160.0f); const ImU32 goldEdge = IM_COL32(255, 210, 50, alpha); const ImU32 goldFade = IM_COL32(255, 210, 50, 0); const float thickness = std::min(W, H) * 0.18f; @@ -1013,9 +862,9 @@ void GameScreen::render(game::GameHandler& gameHandler) { goldFade, goldEdge, goldEdge, goldFade); // "Level X!" text in the center during the first half of the animation - if (levelUpFlashAlpha_ > 0.5f && levelUpDisplayLevel_ > 0) { + if (toastManager_.levelUpFlashAlpha > 0.5f && toastManager_.levelUpDisplayLevel > 0) { char lvlText[32]; - snprintf(lvlText, sizeof(lvlText), "Level %u!", levelUpDisplayLevel_); + snprintf(lvlText, sizeof(lvlText), "Level %u!", toastManager_.levelUpDisplayLevel); ImVec2 ts = ImGui::CalcTextSize(lvlText); float tx = (W - ts.x) * 0.5f; float ty = H * 0.35f; @@ -8685,283 +8534,6 @@ void GameScreen::renderUIErrors(game::GameHandler& /*gameHandler*/, float deltaT ImGui::PopStyleVar(); } -// ============================================================ -// Reputation change toasts -// ============================================================ - -void GameScreen::renderRepToasts(float deltaTime) { - for (auto& e : repToasts_) e.age += deltaTime; - repToasts_.erase( - std::remove_if(repToasts_.begin(), repToasts_.end(), - [](const RepToastEntry& e) { return e.age >= kRepToastLifetime; }), - repToasts_.end()); - - if (repToasts_.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; - - // Stack toasts in the lower-right corner (above the action bar), newest on top - const float toastW = 220.0f; - const float toastH = 26.0f; - const float padY = 4.0f; - const float rightEdge = screenW - 14.0f; - const float baseY = screenH - 180.0f; - - const int count = static_cast(repToasts_.size()); - - ImDrawList* draw = ImGui::GetForegroundDrawList(); - ImFont* font = ImGui::GetFont(); - float fontSize = ImGui::GetFontSize(); - - // Compute standing tier label (Exalted, Revered, Honored, Friendly, Neutral, Unfriendly, Hostile, Hated) - auto standingLabel = [](int32_t s) -> const char* { - if (s >= 42000) return "Exalted"; - if (s >= 21000) return "Revered"; - if (s >= 9000) return "Honored"; - if (s >= 3000) return "Friendly"; - if (s >= 0) return "Neutral"; - if (s >= -3000) return "Unfriendly"; - if (s >= -6000) return "Hostile"; - return "Hated"; - }; - - for (int i = 0; i < count; ++i) { - const auto& e = repToasts_[i]; - // Slide in from right on appear, slide out at end - constexpr float kSlideDur = 0.3f; - float slideIn = std::min(e.age, kSlideDur) / kSlideDur; - float slideOut = std::min(std::max(0.0f, kRepToastLifetime - e.age), kSlideDur) / kSlideDur; - float slide = std::min(slideIn, slideOut); - - float alpha = std::clamp(slide, 0.0f, 1.0f); - float xFull = rightEdge - 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 - draw->AddRectFilled(tl, br, IM_COL32(15, 15, 20, static_cast(alpha * 200)), 4.0f); - // Border: green for gain, red for loss - ImU32 borderCol = (e.delta > 0) - ? IM_COL32(80, 200, 80, static_cast(alpha * 220)) - : IM_COL32(200, 60, 60, static_cast(alpha * 220)); - draw->AddRect(tl, br, borderCol, 4.0f, 0, 1.5f); - - // Delta text: "+250" or "-250" - char deltaBuf[16]; - snprintf(deltaBuf, sizeof(deltaBuf), "%+d", e.delta); - ImU32 deltaCol = (e.delta > 0) ? IM_COL32(80, 220, 80, static_cast(alpha * 255)) - : IM_COL32(220, 70, 70, static_cast(alpha * 255)); - draw->AddText(font, fontSize, ImVec2(tl.x + 6.0f, tl.y + (toastH - fontSize) * 0.5f), - deltaCol, deltaBuf); - - // Faction name + standing - char nameBuf[64]; - snprintf(nameBuf, sizeof(nameBuf), "%s (%s)", e.factionName.c_str(), standingLabel(e.standing)); - draw->AddText(font, fontSize * 0.85f, ImVec2(tl.x + 44.0f, tl.y + (toastH - fontSize * 0.85f) * 0.5f), - IM_COL32(210, 210, 210, static_cast(alpha * 220)), nameBuf); - } -} - -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, static_cast(alpha * 210)), 5.0f); - draw->AddRect(tl, br, IM_COL32(220, 180, 30, static_cast(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, static_cast(alpha * 230))); - draw->AddCircle (ImVec2(iconCx, iconCy), 7.0f, IM_COL32(255, 220, 50, static_cast(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, static_cast(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, static_cast(alpha * 220)), titleStr); - } -} - -// ============================================================ -// Zone Entry Toast -// ============================================================ - -void GameScreen::renderZoneToasts(float deltaTime) { - for (auto& e : zoneToasts_) e.age += deltaTime; - zoneToasts_.erase( - std::remove_if(zoneToasts_.begin(), zoneToasts_.end(), - [](const ZoneToastEntry& e) { return e.age >= kZoneToastLifetime; }), - zoneToasts_.end()); - - // Suppress toasts while the zone text overlay is showing the same zone — - // avoids duplicate "Entering: Stormwind City" messages. - if (zoneTextTimer_ > 0.0f) { - zoneToasts_.erase( - std::remove_if(zoneToasts_.begin(), zoneToasts_.end(), - [this](const ZoneToastEntry& e) { return e.zoneName == zoneTextName_; }), - zoneToasts_.end()); - } - - if (zoneToasts_.empty()) return; - - auto* window = core::Application::getInstance().getWindow(); - float screenW = window ? static_cast(window->getWidth()) : 1280.0f; - - ImDrawList* draw = ImGui::GetForegroundDrawList(); - ImFont* font = ImGui::GetFont(); - - for (int i = 0; i < static_cast(zoneToasts_.size()); ++i) { - const auto& e = zoneToasts_[i]; - constexpr float kSlideDur = 0.35f; - float slideIn = std::min(e.age, kSlideDur) / kSlideDur; - float slideOut = std::min(std::max(0.0f, kZoneToastLifetime - e.age), kSlideDur) / kSlideDur; - float slide = std::min(slideIn, slideOut); - float alpha = std::clamp(slide, 0.0f, 1.0f); - - // Measure text to size the toast - ImVec2 nameSz = font->CalcTextSizeA(14.0f, FLT_MAX, 0.0f, e.zoneName.c_str()); - const char* header = "Entering:"; - ImVec2 hdrSz = font->CalcTextSizeA(11.0f, FLT_MAX, 0.0f, header); - - float toastW = std::max(nameSz.x, hdrSz.x) + 28.0f; - float toastH = 42.0f; - - // Center the toast horizontally, appear just below the zone name area (top-center) - float toastX = (screenW - toastW) * 0.5f; - float toastY = 56.0f + i * (toastH + 4.0f); - // Slide down from above - float offY = (1.0f - slide) * (-toastH - 10.0f); - toastY += offY; - - ImVec2 tl(toastX, toastY); - ImVec2 br(toastX + toastW, toastY + toastH); - - draw->AddRectFilled(tl, br, IM_COL32(10, 10, 16, static_cast(alpha * 200)), 6.0f); - draw->AddRect(tl, br, IM_COL32(160, 140, 80, static_cast(alpha * 220)), 6.0f, 0, 1.2f); - - float cx = tl.x + toastW * 0.5f; - draw->AddText(font, 11.0f, - ImVec2(cx - hdrSz.x * 0.5f, tl.y + 5.0f), - IM_COL32(180, 170, 120, static_cast(alpha * 200)), header); - draw->AddText(font, 14.0f, - ImVec2(cx - nameSz.x * 0.5f, tl.y + toastH * 0.5f + 1.0f), - IM_COL32(255, 230, 140, static_cast(alpha * 240)), e.zoneName.c_str()); - } -} - -// ─── 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, static_cast(alpha * 190)), 5.0f); - draw->AddRect(tl, br, IM_COL32(100, 160, 220, static_cast(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, static_cast(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, static_cast(alpha * 240)), t.text.c_str()); - } -} // ============================================================ // Boss Encounter Frames @@ -18074,808 +17646,6 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { if (!open) gameHandler.closeAuctionHouse(); } -// ============================================================ -// Level-Up Ding Animation -// ============================================================ - -void GameScreen::triggerDing(uint32_t newLevel, uint32_t hpDelta, uint32_t manaDelta, - uint32_t str, uint32_t agi, uint32_t sta, - uint32_t intel, uint32_t spi) { - dingTimer_ = DING_DURATION; - dingLevel_ = newLevel; - dingHpDelta_ = hpDelta; - dingManaDelta_ = manaDelta; - dingStats_[0] = str; - dingStats_[1] = agi; - dingStats_[2] = sta; - dingStats_[3] = intel; - dingStats_[4] = spi; - - auto* renderer = core::Application::getInstance().getRenderer(); - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { - sfx->playLevelUp(); - } - renderer->playEmote("cheer"); - } -} - -void GameScreen::renderDingEffect() { - if (dingTimer_ <= 0.0f) return; - - float dt = ImGui::GetIO().DeltaTime; - dingTimer_ -= dt; - if (dingTimer_ < 0.0f) dingTimer_ = 0.0f; - - // Show "You have reached level X!" for the first 2.5s, fade out over last 0.5s. - // The 3D visual effect is handled by Renderer::triggerLevelUpEffect (LevelUp.m2). - constexpr float kFadeTime = 0.5f; - float alpha = dingTimer_ < kFadeTime ? (dingTimer_ / kFadeTime) : 1.0f; - if (alpha <= 0.0f) return; - - ImGuiIO& io = ImGui::GetIO(); - float cx = io.DisplaySize.x * 0.5f; - float cy = io.DisplaySize.y * 0.38f; // Upper-center, like WoW - - ImDrawList* draw = ImGui::GetForegroundDrawList(); - ImFont* font = ImGui::GetFont(); - float baseSize = ImGui::GetFontSize(); - float fontSize = baseSize * 1.8f; - - char buf[64]; - snprintf(buf, sizeof(buf), "You have reached level %u!", dingLevel_); - - ImVec2 sz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, buf); - float tx = cx - sz.x * 0.5f; - float ty = cy - sz.y * 0.5f; - - // Slight black outline for readability - draw->AddText(font, fontSize, ImVec2(tx + 2, ty + 2), - IM_COL32(0, 0, 0, static_cast(alpha * 180)), buf); - // Gold text - draw->AddText(font, fontSize, ImVec2(tx, ty), - IM_COL32(255, 210, 0, static_cast(alpha * 255)), buf); - - // Stat gains below the main text (shown only if server sent deltas) - bool hasStatGains = (dingHpDelta_ > 0 || dingManaDelta_ > 0 || - dingStats_[0] || dingStats_[1] || dingStats_[2] || - dingStats_[3] || dingStats_[4]); - if (hasStatGains) { - float smallSize = baseSize * 0.95f; - float yOff = ty + sz.y + 6.0f; - - // Build stat delta string: "+150 HP +80 Mana +2 Str +2 Agi ..." - static constexpr const char* kStatLabels[] = { "Str", "Agi", "Sta", "Int", "Spi" }; - char statBuf[128]; - int written = 0; - if (dingHpDelta_ > 0) - written += snprintf(statBuf + written, sizeof(statBuf) - written, - "+%u HP ", dingHpDelta_); - if (dingManaDelta_ > 0) - written += snprintf(statBuf + written, sizeof(statBuf) - written, - "+%u Mana ", dingManaDelta_); - for (int i = 0; i < 5 && written < static_cast(sizeof(statBuf)) - 1; ++i) { - if (dingStats_[i] > 0) - written += snprintf(statBuf + written, sizeof(statBuf) - written, - "+%u %s ", dingStats_[i], kStatLabels[i]); - } - // Trim trailing spaces - while (written > 0 && statBuf[written - 1] == ' ') --written; - statBuf[written] = '\0'; - - if (written > 0) { - ImVec2 ssz = font->CalcTextSizeA(smallSize, FLT_MAX, 0.0f, statBuf); - float stx = cx - ssz.x * 0.5f; - draw->AddText(font, smallSize, ImVec2(stx + 1, yOff + 1), - IM_COL32(0, 0, 0, static_cast(alpha * 160)), statBuf); - draw->AddText(font, smallSize, ImVec2(stx, yOff), - IM_COL32(100, 220, 100, static_cast(alpha * 230)), statBuf); - } - } -} - -void GameScreen::triggerAchievementToast(uint32_t achievementId, std::string name) { - achievementToastId_ = achievementId; - achievementToastName_ = std::move(name); - achievementToastTimer_ = ACHIEVEMENT_TOAST_DURATION; - - // Play a UI sound if available - auto* renderer = core::Application::getInstance().getRenderer(); - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { - sfx->playAchievementAlert(); - } - } -} - -void GameScreen::renderAchievementToast() { - if (achievementToastTimer_ <= 0.0f) return; - - float dt = ImGui::GetIO().DeltaTime; - achievementToastTimer_ -= dt; - if (achievementToastTimer_ < 0.0f) achievementToastTimer_ = 0.0f; - - auto* window = core::Application::getInstance().getWindow(); - float screenW = window ? static_cast(window->getWidth()) : 1280.0f; - float screenH = window ? static_cast(window->getHeight()) : 720.0f; - - // Slide in from the right — fully visible for most of the duration, slides out at end - constexpr float SLIDE_TIME = 0.4f; - float slideIn = std::min(achievementToastTimer_, ACHIEVEMENT_TOAST_DURATION - achievementToastTimer_); - float slideFrac = (ACHIEVEMENT_TOAST_DURATION > 0.0f && SLIDE_TIME > 0.0f) - ? std::min(slideIn / SLIDE_TIME, 1.0f) - : 1.0f; - - constexpr float TOAST_W = 280.0f; - constexpr float TOAST_H = 60.0f; - float xFull = screenW - TOAST_W - 20.0f; - float xHidden = screenW + 10.0f; - float toastX = xHidden + (xFull - xHidden) * slideFrac; - float toastY = screenH - TOAST_H - 80.0f; // above action bar area - - float alpha = std::min(1.0f, achievementToastTimer_ / 0.5f); // fade at very end - - ImDrawList* draw = ImGui::GetForegroundDrawList(); - - // Background panel (gold border, dark fill) - ImVec2 tl(toastX, toastY); - ImVec2 br(toastX + TOAST_W, toastY + TOAST_H); - draw->AddRectFilled(tl, br, IM_COL32(30, 20, 10, static_cast(alpha * 230)), 6.0f); - draw->AddRect(tl, br, IM_COL32(200, 170, 50, static_cast(alpha * 255)), 6.0f, 0, 2.0f); - - // Title - ImFont* font = ImGui::GetFont(); - float titleSize = 14.0f; - float bodySize = 12.0f; - const char* title = "Achievement Earned!"; - float titleW = font->CalcTextSizeA(titleSize, FLT_MAX, 0.0f, title).x; - float titleX = toastX + (TOAST_W - titleW) * 0.5f; - draw->AddText(font, titleSize, ImVec2(titleX + 1, toastY + 8 + 1), - IM_COL32(0, 0, 0, static_cast(alpha * 180)), title); - draw->AddText(font, titleSize, ImVec2(titleX, toastY + 8), - IM_COL32(255, 215, 0, static_cast(alpha * 255)), title); - - // Achievement name (falls back to ID if name not available) - char idBuf[256]; - const char* achText = achievementToastName_.empty() - ? nullptr : achievementToastName_.c_str(); - if (achText) { - std::snprintf(idBuf, sizeof(idBuf), "%s", achText); - } else { - std::snprintf(idBuf, sizeof(idBuf), "Achievement #%u", achievementToastId_); - } - float idW = font->CalcTextSizeA(bodySize, FLT_MAX, 0.0f, idBuf).x; - float idX = toastX + (TOAST_W - idW) * 0.5f; - draw->AddText(font, bodySize, ImVec2(idX, toastY + 28), - IM_COL32(220, 200, 150, static_cast(alpha * 255)), idBuf); -} - -// --------------------------------------------------------------------------- -// Area discovery toast — "Discovered: ! (+XP XP)" centered on screen -// --------------------------------------------------------------------------- - -void GameScreen::renderDiscoveryToast() { - if (discoveryToastTimer_ <= 0.0f) return; - - float dt = ImGui::GetIO().DeltaTime; - discoveryToastTimer_ -= dt; - if (discoveryToastTimer_ < 0.0f) discoveryToastTimer_ = 0.0f; - - // Fade: ramp up in first 0.4s, hold, fade out in last 1.0s - float alpha; - if (discoveryToastTimer_ > DISCOVERY_TOAST_DURATION - 0.4f) - alpha = 1.0f - (discoveryToastTimer_ - (DISCOVERY_TOAST_DURATION - 0.4f)) / 0.4f; - else if (discoveryToastTimer_ < 1.0f) - alpha = discoveryToastTimer_; - else - alpha = 1.0f; - alpha = std::clamp(alpha, 0.0f, 1.0f); - - auto* window = core::Application::getInstance().getWindow(); - float screenW = window ? static_cast(window->getWidth()) : 1280.0f; - float screenH = window ? static_cast(window->getHeight()) : 720.0f; - - ImFont* font = ImGui::GetFont(); - ImDrawList* draw = ImGui::GetForegroundDrawList(); - - const char* header = "Discovered!"; - float headerSize = 16.0f; - float nameSize = 28.0f; - float xpSize = 14.0f; - - ImVec2 headerDim = font->CalcTextSizeA(headerSize, FLT_MAX, 0.0f, header); - ImVec2 nameDim = font->CalcTextSizeA(nameSize, FLT_MAX, 0.0f, discoveryToastName_.c_str()); - - char xpBuf[48]; - if (discoveryToastXP_ > 0) - snprintf(xpBuf, sizeof(xpBuf), "+%u XP", discoveryToastXP_); - else - xpBuf[0] = '\0'; - ImVec2 xpDim = font->CalcTextSizeA(xpSize, FLT_MAX, 0.0f, xpBuf); - - // Position slightly below zone text (at 37% down screen) - float centreY = screenH * 0.37f; - float headerX = (screenW - headerDim.x) * 0.5f; - float nameX = (screenW - nameDim.x) * 0.5f; - float xpX = (screenW - xpDim.x) * 0.5f; - float headerY = centreY; - float nameY = centreY + headerDim.y + 4.0f; - float xpY = nameY + nameDim.y + 4.0f; - - // "Discovered!" in gold - draw->AddText(font, headerSize, ImVec2(headerX + 1, headerY + 1), - IM_COL32(0, 0, 0, static_cast(alpha * 160)), header); - draw->AddText(font, headerSize, ImVec2(headerX, headerY), - IM_COL32(255, 215, 0, static_cast(alpha * 255)), header); - - // Area name in white - draw->AddText(font, nameSize, ImVec2(nameX + 1, nameY + 1), - IM_COL32(0, 0, 0, static_cast(alpha * 160)), discoveryToastName_.c_str()); - draw->AddText(font, nameSize, ImVec2(nameX, nameY), - IM_COL32(255, 255, 255, static_cast(alpha * 255)), discoveryToastName_.c_str()); - - // XP gain in light green (if any) - if (xpBuf[0] != '\0') { - draw->AddText(font, xpSize, ImVec2(xpX + 1, xpY + 1), - IM_COL32(0, 0, 0, static_cast(alpha * 140)), xpBuf); - draw->AddText(font, xpSize, ImVec2(xpX, xpY), - IM_COL32(100, 220, 100, static_cast(alpha * 230)), xpBuf); - } -} - -// --------------------------------------------------------------------------- -// 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); - } -} - -// --------------------------------------------------------------------------- -// Item loot toasts — quality-coloured strip at bottom-left when item received -// --------------------------------------------------------------------------- - -void GameScreen::renderItemLootToasts() { - if (itemLootToasts_.empty()) return; - - float dt = ImGui::GetIO().DeltaTime; - for (auto& t : itemLootToasts_) t.age += dt; - itemLootToasts_.erase( - std::remove_if(itemLootToasts_.begin(), itemLootToasts_.end(), - [](const ItemLootToastEntry& t) { return t.age >= ITEM_LOOT_TOAST_DURATION; }), - itemLootToasts_.end()); - if (itemLootToasts_.empty()) return; - - ImVec2 displaySize = ImGui::GetIO().DisplaySize; - float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; - - // Quality colours (matching WoW convention) - static const ImU32 kQualityColors[] = { - IM_COL32(157, 157, 157, 255), // 0 grey (poor) - IM_COL32(255, 255, 255, 255), // 1 white (common) - IM_COL32( 30, 255, 30, 255), // 2 green (uncommon) - IM_COL32( 0, 112, 221, 255), // 3 blue (rare) - IM_COL32(163, 53, 238, 255), // 4 purple (epic) - IM_COL32(255, 128, 0, 255), // 5 orange (legendary) - IM_COL32(230, 204, 128, 255), // 6 light gold (artifact) - IM_COL32(230, 204, 128, 255), // 7 light gold (heirloom) - }; - - // Stack at bottom-left above action bars; each item is 24 px tall - constexpr float TOAST_W = 260.0f; - constexpr float TOAST_H = 24.0f; - constexpr float TOAST_GAP = 2.0f; - constexpr float TOAST_X = 14.0f; - float baseY = screenH * 0.68f; // slightly above the whisper toasts - - ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); - const int count = static_cast(itemLootToasts_.size()); - - for (int i = 0; i < count; ++i) { - const auto& toast = itemLootToasts_[i]; - - float remaining = ITEM_LOOT_TOAST_DURATION - toast.age; - float alpha; - if (toast.age < 0.15f) - alpha = toast.age / 0.15f; - else if (remaining < 0.7f) - alpha = remaining / 0.7f; - else - alpha = 1.0f; - alpha = std::clamp(alpha, 0.0f, 1.0f); - - // Slide-in from left - float slideX = (toast.age < 0.15f) ? (TOAST_W * (1.0f - toast.age / 0.15f)) : 0.0f; - float tx = TOAST_X - slideX; - float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP); - - uint8_t bgA = static_cast(180 * alpha); - uint8_t fgA = static_cast(255 * alpha); - - // Background: very dark with quality-tinted left border accent - bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H), - IM_COL32(12, 12, 12, bgA), 3.0f); - - // Quality colour accent bar on left edge (3px wide) - ImU32 qualCol = kQualityColors[std::min(static_cast(7u), toast.quality)]; - ImU32 qualColA = (qualCol & 0x00FFFFFFu) | (static_cast(fgA) << 24u); - bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + 3.0f, ty + TOAST_H), qualColA, 3.0f); - - // "Loot:" label in dim white - bgDL->AddText(ImVec2(tx + 7.0f, ty + 5.0f), - IM_COL32(160, 160, 160, static_cast(200 * alpha)), "Loot:"); - - // Item name in quality colour - std::string displayName = toast.name.empty() ? ("Item #" + std::to_string(toast.itemId)) : toast.name; - if (displayName.size() > 26) { displayName.resize(23); displayName += "..."; } - bgDL->AddText(ImVec2(tx + 42.0f, ty + 5.0f), qualColA, displayName.c_str()); - - // Count (if > 1) - if (toast.count > 1) { - char countBuf[12]; - snprintf(countBuf, sizeof(countBuf), "x%u", toast.count); - bgDL->AddText(ImVec2(tx + TOAST_W - 34.0f, ty + 5.0f), - IM_COL32(200, 200, 200, static_cast(200 * alpha)), countBuf); - } - } -} - -// --------------------------------------------------------------------------- -// PvP honor credit toasts — shown at screen top-right on honorable kill -// --------------------------------------------------------------------------- - -void GameScreen::renderPvpHonorToasts() { - if (pvpHonorToasts_.empty()) return; - - float dt = ImGui::GetIO().DeltaTime; - for (auto& t : pvpHonorToasts_) t.age += dt; - pvpHonorToasts_.erase( - std::remove_if(pvpHonorToasts_.begin(), pvpHonorToasts_.end(), - [](const PvpHonorToastEntry& t) { return t.age >= PVP_HONOR_TOAST_DURATION; }), - pvpHonorToasts_.end()); - if (pvpHonorToasts_.empty()) return; - - ImVec2 displaySize = ImGui::GetIO().DisplaySize; - float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; - - // Stack toasts at top-right, below any minimap area - constexpr float TOAST_W = 180.0f; - constexpr float TOAST_H = 30.0f; - constexpr float TOAST_GAP = 3.0f; - constexpr float TOAST_TOP = 10.0f; - float toastX = screenW - TOAST_W - 10.0f; - - ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); - const int count = static_cast(pvpHonorToasts_.size()); - - for (int i = 0; i < count; ++i) { - const auto& toast = pvpHonorToasts_[i]; - - float remaining = PVP_HONOR_TOAST_DURATION - toast.age; - float alpha; - if (toast.age < 0.15f) - alpha = toast.age / 0.15f; - else if (remaining < 0.8f) - alpha = remaining / 0.8f; - else - alpha = 1.0f; - alpha = std::clamp(alpha, 0.0f, 1.0f); - - float ty = TOAST_TOP + i * (TOAST_H + TOAST_GAP); - - uint8_t bgA = static_cast(190 * alpha); - uint8_t fgA = static_cast(255 * alpha); - - // Background: dark red (PvP theme) - bgDL->AddRectFilled(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), - IM_COL32(28, 5, 5, bgA), 4.0f); - bgDL->AddRect(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), - IM_COL32(200, 50, 50, static_cast(160 * alpha)), 4.0f, 0, 1.2f); - - // Sword ⚔ icon (U+2694, UTF-8: e2 9a 94) - bgDL->AddText(ImVec2(toastX + 7.0f, ty + 7.0f), - IM_COL32(220, 80, 80, fgA), "\xe2\x9a\x94"); - - // "+N Honor" text in gold - char buf[40]; - snprintf(buf, sizeof(buf), "+%u Honor", toast.honor); - bgDL->AddText(ImVec2(toastX + 24.0f, ty + 8.0f), - IM_COL32(255, 210, 50, fgA), buf); - } -} - -// --------------------------------------------------------------------------- -// Nearby player level-up toasts — shown at screen bottom-centre -// --------------------------------------------------------------------------- - -void GameScreen::renderPlayerLevelUpToasts(game::GameHandler& gameHandler) { - if (playerLevelUpToasts_.empty()) return; - - float dt = ImGui::GetIO().DeltaTime; - for (auto& t : playerLevelUpToasts_) { - t.age += dt; - // Lazy name resolution — fill in once the name cache has it - if (t.playerName.empty() && t.guid != 0) { - t.playerName = gameHandler.lookupName(t.guid); - } - } - playerLevelUpToasts_.erase( - std::remove_if(playerLevelUpToasts_.begin(), playerLevelUpToasts_.end(), - [](const PlayerLevelUpToastEntry& t) { - return t.age >= PLAYER_LEVELUP_TOAST_DURATION; - }), - playerLevelUpToasts_.end()); - if (playerLevelUpToasts_.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 toasts at screen bottom-centre, above action bars - constexpr float TOAST_W = 230.0f; - constexpr float TOAST_H = 38.0f; - constexpr float TOAST_GAP = 4.0f; - float baseY = screenH * 0.72f; - float toastX = (screenW - TOAST_W) * 0.5f; - - ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); - const int count = static_cast(playerLevelUpToasts_.size()); - - for (int i = 0; i < count; ++i) { - const auto& toast = playerLevelUpToasts_[i]; - - float remaining = PLAYER_LEVELUP_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); - - // Subtle pop-up from below during first 0.2s - float slideY = (toast.age < 0.2f) ? (TOAST_H * (1.0f - toast.age / 0.2f)) : 0.0f; - float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP) + slideY; - - uint8_t bgA = static_cast(200 * alpha); - uint8_t fgA = static_cast(255 * alpha); - - // Background: dark gold tint - bgDL->AddRectFilled(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), - IM_COL32(30, 22, 5, bgA), 5.0f); - // Gold border with glow at peak - float glowStr = (toast.age < 0.5f) ? (1.0f - toast.age / 0.5f) : 0.0f; - uint8_t borderA = static_cast((160 + 80 * glowStr) * alpha); - bgDL->AddRect(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), - IM_COL32(255, 210, 50, borderA), 5.0f, 0, 1.5f + glowStr * 1.5f); - - // Star ★ icon on left - bgDL->AddText(ImVec2(toastX + 8.0f, ty + 10.0f), - IM_COL32(255, 220, 60, fgA), "\xe2\x98\x85"); // UTF-8 ★ - - // " is now level X!" text - const char* displayName = toast.playerName.empty() ? "A player" : toast.playerName.c_str(); - char buf[64]; - snprintf(buf, sizeof(buf), "%.18s is now level %u!", displayName, toast.newLevel); - bgDL->AddText(ImVec2(toastX + 26.0f, ty + 11.0f), - IM_COL32(255, 230, 100, fgA), buf); - } -} - -// --------------------------------------------------------------------------- -// Resurrection flash — brief screen brightening + "You have been resurrected!" -// banner when the player transitions from ghost back to alive. -// --------------------------------------------------------------------------- - -void GameScreen::renderResurrectFlash() { - if (resurrectFlashTimer_ <= 0.0f) return; - - float dt = ImGui::GetIO().DeltaTime; - resurrectFlashTimer_ -= dt; - if (resurrectFlashTimer_ <= 0.0f) { - resurrectFlashTimer_ = 0.0f; - 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; - - // Normalised age in [0, 1] (0 = just fired, 1 = fully elapsed) - float t = 1.0f - resurrectFlashTimer_ / kResurrectFlashDuration; - - // Alpha envelope: fast fade-in (first 0.15s), hold, then fade-out (last 0.8s) - float alpha; - const float fadeIn = 0.15f / kResurrectFlashDuration; // ~5% of lifetime - const float fadeOut = 0.8f / kResurrectFlashDuration; // ~27% of lifetime - if (t < fadeIn) - alpha = t / fadeIn; - else if (t < 1.0f - fadeOut) - alpha = 1.0f; - else - alpha = (1.0f - t) / fadeOut; - alpha = std::clamp(alpha, 0.0f, 1.0f); - - ImDrawList* bg = ImGui::GetBackgroundDrawList(); - - // Soft golden/white vignette — brightening instead of darkening - uint8_t vigA = static_cast(50 * alpha); - bg->AddRectFilled(ImVec2(0, 0), ImVec2(screenW, screenH), - IM_COL32(200, 230, 255, vigA)); - - // Centered banner panel - constexpr float PANEL_W = 360.0f; - constexpr float PANEL_H = 52.0f; - float px = (screenW - PANEL_W) * 0.5f; - float py = screenH * 0.34f; - - uint8_t bgA = static_cast(210 * alpha); - uint8_t borderA = static_cast(255 * alpha); - uint8_t textA = static_cast(255 * alpha); - - // Background: deep blue-black - bg->AddRectFilled(ImVec2(px, py), ImVec2(px + PANEL_W, py + PANEL_H), - IM_COL32(10, 18, 40, bgA), 8.0f); - - // Border glow: bright holy gold - bg->AddRect(ImVec2(px, py), ImVec2(px + PANEL_W, py + PANEL_H), - IM_COL32(200, 230, 100, borderA), 8.0f, 0, 2.0f); - // Inner halo line - bg->AddRect(ImVec2(px + 3.0f, py + 3.0f), ImVec2(px + PANEL_W - 3.0f, py + PANEL_H - 3.0f), - IM_COL32(255, 255, 180, static_cast(80 * alpha)), 6.0f, 0, 1.0f); - - // "✦ You have been resurrected! ✦" centered - // UTF-8 heavy four-pointed star U+2726: \xe2\x9c\xa6 - const char* banner = "\xe2\x9c\xa6 You have been resurrected! \xe2\x9c\xa6"; - ImFont* font = ImGui::GetFont(); - float fontSize = ImGui::GetFontSize(); - ImVec2 textSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, banner); - float tx = px + (PANEL_W - textSz.x) * 0.5f; - float ty = py + (PANEL_H - textSz.y) * 0.5f; - - // Drop shadow - bg->AddText(font, fontSize, ImVec2(tx + 1.0f, ty + 1.0f), - IM_COL32(0, 0, 0, static_cast(180 * alpha)), banner); - // Main text in warm gold - bg->AddText(font, fontSize, ImVec2(tx, ty), - IM_COL32(255, 240, 120, textA), banner); -} - -// --------------------------------------------------------------------------- -// Whisper toast notifications — brief overlay when a player whispers you -// --------------------------------------------------------------------------- - -void GameScreen::renderWhisperToasts() { - if (whisperToasts_.empty()) return; - - float dt = ImGui::GetIO().DeltaTime; - - // Age and prune expired toasts - for (auto& t : whisperToasts_) t.age += dt; - whisperToasts_.erase( - std::remove_if(whisperToasts_.begin(), whisperToasts_.end(), - [](const WhisperToastEntry& t) { return t.age >= WHISPER_TOAST_DURATION; }), - whisperToasts_.end()); - if (whisperToasts_.empty()) return; - - ImVec2 displaySize = ImGui::GetIO().DisplaySize; - float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; - - // Stack toasts at bottom-left, above the action bars (y ≈ screenH * 0.72) - // Each toast is ~56px tall with a 4px gap between them. - constexpr float TOAST_W = 280.0f; - constexpr float TOAST_H = 56.0f; - constexpr float TOAST_GAP = 4.0f; - constexpr float TOAST_X = 14.0f; // left edge (won't cover action bars) - float baseY = screenH * 0.72f; - - ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); - - const int count = static_cast(whisperToasts_.size()); - for (int i = 0; i < count; ++i) { - auto& toast = whisperToasts_[i]; - - // Fade in over 0.25s; fade out in last 1.0s - float alpha; - float remaining = WHISPER_TOAST_DURATION - toast.age; - if (toast.age < 0.25f) - alpha = toast.age / 0.25f; - else if (remaining < 1.0f) - alpha = remaining; - else - alpha = 1.0f; - alpha = std::clamp(alpha, 0.0f, 1.0f); - - // Slide-in from left: offset 0→0 after 0.25s - float slideX = (toast.age < 0.25f) ? (TOAST_W * (1.0f - toast.age / 0.25f)) : 0.0f; - float tx = TOAST_X - slideX; - float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP); - - uint8_t bgA = static_cast(210 * alpha); - uint8_t fgA = static_cast(255 * alpha); - - // Background panel — dark purple tint (whisper color convention) - bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H), - IM_COL32(25, 10, 40, bgA), 6.0f); - // Purple border - bgDL->AddRect(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H), - IM_COL32(160, 80, 220, static_cast(180 * alpha)), 6.0f, 0, 1.5f); - - // "Whisper" label (small, purple-ish) - bgDL->AddText(ImVec2(tx + 10.0f, ty + 6.0f), - IM_COL32(190, 110, 255, fgA), "Whisper from:"); - - // Sender name (gold) - bgDL->AddText(ImVec2(tx + 10.0f, ty + 20.0f), - IM_COL32(255, 210, 50, fgA), toast.sender.c_str()); - - // Message preview (white, dimmer) - bgDL->AddText(ImVec2(tx + 10.0f, ty + 36.0f), - IM_COL32(220, 220, 220, static_cast(200 * alpha)), - toast.preview.c_str()); - } -} - -// Zone discovery text — "Entering: " fades in/out at screen centre -// --------------------------------------------------------------------------- - -void GameScreen::renderZoneText(game::GameHandler& gameHandler) { - // Poll worldStateZoneId for server-driven zone changes (fires on every zone crossing, - // including sub-zones like Ironforge within Dun Morogh). - uint32_t wsZoneId = gameHandler.getWorldStateZoneId(); - if (wsZoneId != 0 && wsZoneId != lastKnownWorldStateZoneId_) { - lastKnownWorldStateZoneId_ = wsZoneId; - std::string wsName = gameHandler.getWhoAreaName(wsZoneId); - if (!wsName.empty()) { - zoneTextName_ = wsName; - zoneTextTimer_ = ZONE_TEXT_DURATION; - } - } - - // Also poll the renderer for zone name changes (covers map-level transitions - // where worldStateZoneId may not change immediately). - auto* appRenderer = core::Application::getInstance().getRenderer(); - if (appRenderer) { - const std::string& zoneName = appRenderer->getCurrentZoneName(); - if (!zoneName.empty() && zoneName != lastKnownZoneName_) { - lastKnownZoneName_ = zoneName; - // Only override if the worldState hasn't already queued this zone - if (zoneTextName_ != zoneName) { - zoneTextName_ = zoneName; - zoneTextTimer_ = ZONE_TEXT_DURATION; - } - } - } - - if (zoneTextTimer_ <= 0.0f || zoneTextName_.empty()) return; - - float dt = ImGui::GetIO().DeltaTime; - zoneTextTimer_ -= dt; - if (zoneTextTimer_ < 0.0f) zoneTextTimer_ = 0.0f; - - auto* window = core::Application::getInstance().getWindow(); - float screenW = window ? static_cast(window->getWidth()) : 1280.0f; - float screenH = window ? static_cast(window->getHeight()) : 720.0f; - - // Fade: ramp up in first 0.5 s, hold, fade out in last 1.0 s - float alpha; - if (zoneTextTimer_ > ZONE_TEXT_DURATION - 0.5f) - alpha = 1.0f - (zoneTextTimer_ - (ZONE_TEXT_DURATION - 0.5f)) / 0.5f; - else if (zoneTextTimer_ < 1.0f) - alpha = zoneTextTimer_; - else - alpha = 1.0f; - alpha = std::clamp(alpha, 0.0f, 1.0f); - - ImFont* font = ImGui::GetFont(); - - // "Entering:" header - const char* header = "Entering:"; - float headerSize = 16.0f; - float nameSize = 26.0f; - - ImVec2 headerDim = font->CalcTextSizeA(headerSize, FLT_MAX, 0.0f, header); - ImVec2 nameDim = font->CalcTextSizeA(nameSize, FLT_MAX, 0.0f, zoneTextName_.c_str()); - - float centreY = screenH * 0.30f; // upper third, like WoW - float headerX = (screenW - headerDim.x) * 0.5f; - float nameX = (screenW - nameDim.x) * 0.5f; - float headerY = centreY; - float nameY = centreY + headerDim.y + 4.0f; - - ImDrawList* draw = ImGui::GetForegroundDrawList(); - - // "Entering:" in gold - draw->AddText(font, headerSize, ImVec2(headerX + 1, headerY + 1), - IM_COL32(0, 0, 0, static_cast(alpha * 160)), header); - draw->AddText(font, headerSize, ImVec2(headerX, headerY), - IM_COL32(255, 215, 0, static_cast(alpha * 255)), header); - - // Zone name in white - draw->AddText(font, nameSize, ImVec2(nameX + 1, nameY + 1), - IM_COL32(0, 0, 0, static_cast(alpha * 160)), zoneTextName_.c_str()); - draw->AddText(font, nameSize, ImVec2(nameX, nameY), - IM_COL32(255, 255, 255, static_cast(alpha * 255)), zoneTextName_.c_str()); -} // --------------------------------------------------------------------------- // Screen-space weather overlay (rain / snow / storm) diff --git a/src/ui/toast_manager.cpp b/src/ui/toast_manager.cpp new file mode 100644 index 00000000..1339b7f1 --- /dev/null +++ b/src/ui/toast_manager.cpp @@ -0,0 +1,1250 @@ +#include "ui/toast_manager.hpp" +#include "game/game_handler.hpp" +#include "core/application.hpp" +#include "rendering/renderer.hpp" +#include "audio/ui_sound_manager.hpp" + +#include +#include +#include +#include + +namespace wowee { namespace ui { + +// --------------------------------------------------------------------------- +// Setup toast callbacks on GameHandler (idempotent — safe to call every frame) +// --------------------------------------------------------------------------- +void ToastManager::setupCallbacks(game::GameHandler& gameHandler) { + // NOTE: Level-up and achievement callbacks are registered by Application + // (which also triggers the 3D level-up effect). Application routes to + // triggerDing() / triggerAchievementToast() via the public API. + + // Area discovery toast callback + if (!areaDiscoveryCallbackSet_) { + gameHandler.setAreaDiscoveryCallback([this](const std::string& areaName, uint32_t xpGained) { + discoveryToastName_ = areaName.empty() ? "New Area" : areaName; + discoveryToastXP_ = xpGained; + discoveryToastTimer_ = DISCOVERY_TOAST_DURATION; + }); + areaDiscoveryCallbackSet_ = true; + } + + // Quest objective progress toast callback + if (!questProgressCallbackSet_) { + gameHandler.setQuestProgressCallback([this](const std::string& questTitle, + const std::string& objectiveName, + uint32_t current, uint32_t required) { + for (auto& t : questToasts_) { + if (t.questTitle == questTitle && t.objectiveName == objectiveName) { + t.current = current; + t.required = required; + t.age = 0.0f; + return; + } + } + if (questToasts_.size() >= 4) questToasts_.erase(questToasts_.begin()); + questToasts_.push_back({questTitle, objectiveName, current, required, 0.0f}); + }); + questProgressCallbackSet_ = true; + } + + // Other-player level-up toast callback + if (!otherPlayerLevelUpCallbackSet_) { + gameHandler.setOtherPlayerLevelUpCallback([this](uint64_t guid, uint32_t newLevel) { + for (auto& t : playerLevelUpToasts_) { + if (t.guid == guid) { + t.newLevel = newLevel; + t.age = 0.0f; + return; + } + } + if (playerLevelUpToasts_.size() >= 3) + playerLevelUpToasts_.erase(playerLevelUpToasts_.begin()); + playerLevelUpToasts_.push_back({guid, "", newLevel, 0.0f}); + }); + otherPlayerLevelUpCallbackSet_ = true; + } + + // PvP honor credit toast callback + if (!pvpHonorCallbackSet_) { + gameHandler.setPvpHonorCallback([this](uint32_t honor, uint64_t /*victimGuid*/, uint32_t rank) { + if (honor == 0) return; + pvpHonorToasts_.push_back({honor, rank, 0.0f}); + if (pvpHonorToasts_.size() > 4) + pvpHonorToasts_.erase(pvpHonorToasts_.begin()); + }); + pvpHonorCallbackSet_ = true; + } + + // Item loot toast callback + if (!itemLootCallbackSet_) { + gameHandler.setItemLootCallback([this](uint32_t itemId, uint32_t count, + uint32_t quality, const std::string& name) { + for (auto& t : itemLootToasts_) { + if (t.itemId == itemId) { + t.count += count; + t.age = 0.0f; + return; + } + } + if (itemLootToasts_.size() >= 5) + itemLootToasts_.erase(itemLootToasts_.begin()); + itemLootToasts_.push_back({itemId, count, quality, name, 0.0f}); + }); + itemLootCallbackSet_ = true; + } + + // Ghost-state callback to flash "You have been resurrected!" on revival + if (!ghostStateCallbackSet_) { + gameHandler.setGhostStateCallback([this](bool isGhost) { + if (!isGhost) { + resurrectFlashTimer_ = kResurrectFlashDuration; + } + }); + ghostStateCallbackSet_ = true; + } + + // Reputation change toast callback + if (!repChangeCallbackSet_) { + gameHandler.setRepChangeCallback([this](const std::string& name, int32_t delta, int32_t standing) { + repToasts_.push_back({name, delta, standing, 0.0f}); + if (repToasts_.size() > 4) repToasts_.erase(repToasts_.begin()); + }); + repChangeCallbackSet_ = true; + } + + // Quest completion toast callback + 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; + } +} + +// --------------------------------------------------------------------------- +// Render early toasts (before action bars) +// --------------------------------------------------------------------------- +void ToastManager::renderEarlyToasts(float deltaTime, game::GameHandler& gameHandler) { + // Zone entry detection — fire a toast when the renderer's zone name changes + if (auto* rend = core::Application::getInstance().getRenderer()) { + const std::string& curZone = rend->getCurrentZoneName(); + if (!curZone.empty() && curZone != lastKnownZone_) { + if (!lastKnownZone_.empty()) { + zoneToasts_.push_back({curZone, 0.0f}); + if (zoneToasts_.size() > 3) + zoneToasts_.erase(zoneToasts_.begin()); + } + lastKnownZone_ = curZone; + } + } + + renderRepToasts(deltaTime); + renderQuestCompleteToasts(deltaTime); + renderZoneToasts(deltaTime); + renderAreaTriggerToasts(deltaTime, gameHandler); +} + +// --------------------------------------------------------------------------- +// Render late toasts (after escape menu / settings) +// --------------------------------------------------------------------------- +void ToastManager::renderLateToasts(game::GameHandler& gameHandler) { + renderDingEffect(); + renderAchievementToast(); + renderDiscoveryToast(); + renderWhisperToasts(); + renderQuestProgressToasts(); + renderPlayerLevelUpToasts(gameHandler); + renderPvpHonorToasts(); + renderItemLootToasts(); + renderResurrectFlash(); + renderZoneText(gameHandler); +} + +// ============================================================ +// Reputation change toasts +// ============================================================ + +void ToastManager::renderRepToasts(float deltaTime) { + for (auto& e : repToasts_) e.age += deltaTime; + repToasts_.erase( + std::remove_if(repToasts_.begin(), repToasts_.end(), + [](const RepToastEntry& e) { return e.age >= kRepToastLifetime; }), + repToasts_.end()); + + if (repToasts_.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; + + // Stack toasts in the lower-right corner (above the action bar), newest on top + const float toastW = 220.0f; + const float toastH = 26.0f; + const float padY = 4.0f; + const float rightEdge = screenW - 14.0f; + const float baseY = screenH - 180.0f; + + const int count = static_cast(repToasts_.size()); + + ImDrawList* draw = ImGui::GetForegroundDrawList(); + ImFont* font = ImGui::GetFont(); + float fontSize = ImGui::GetFontSize(); + + // Compute standing tier label (Exalted, Revered, Honored, Friendly, Neutral, Unfriendly, Hostile, Hated) + auto standingLabel = [](int32_t s) -> const char* { + if (s >= 42000) return "Exalted"; + if (s >= 21000) return "Revered"; + if (s >= 9000) return "Honored"; + if (s >= 3000) return "Friendly"; + if (s >= 0) return "Neutral"; + if (s >= -3000) return "Unfriendly"; + if (s >= -6000) return "Hostile"; + return "Hated"; + }; + + for (int i = 0; i < count; ++i) { + const auto& e = repToasts_[i]; + // Slide in from right on appear, slide out at end + constexpr float kSlideDur = 0.3f; + float slideIn = std::min(e.age, kSlideDur) / kSlideDur; + float slideOut = std::min(std::max(0.0f, kRepToastLifetime - e.age), kSlideDur) / kSlideDur; + float slide = std::min(slideIn, slideOut); + + float alpha = std::clamp(slide, 0.0f, 1.0f); + float xFull = rightEdge - 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 + draw->AddRectFilled(tl, br, IM_COL32(15, 15, 20, static_cast(alpha * 200)), 4.0f); + // Border: green for gain, red for loss + ImU32 borderCol = (e.delta > 0) + ? IM_COL32(80, 200, 80, static_cast(alpha * 220)) + : IM_COL32(200, 60, 60, static_cast(alpha * 220)); + draw->AddRect(tl, br, borderCol, 4.0f, 0, 1.5f); + + // Delta text: "+250" or "-250" + char deltaBuf[16]; + snprintf(deltaBuf, sizeof(deltaBuf), "%+d", e.delta); + ImU32 deltaCol = (e.delta > 0) ? IM_COL32(80, 220, 80, static_cast(alpha * 255)) + : IM_COL32(220, 70, 70, static_cast(alpha * 255)); + draw->AddText(font, fontSize, ImVec2(tl.x + 6.0f, tl.y + (toastH - fontSize) * 0.5f), + deltaCol, deltaBuf); + + // Faction name + standing + char nameBuf[64]; + snprintf(nameBuf, sizeof(nameBuf), "%s (%s)", e.factionName.c_str(), standingLabel(e.standing)); + draw->AddText(font, fontSize * 0.85f, ImVec2(tl.x + 44.0f, tl.y + (toastH - fontSize * 0.85f) * 0.5f), + IM_COL32(210, 210, 210, static_cast(alpha * 220)), nameBuf); + } +} + +void ToastManager::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, static_cast(alpha * 210)), 5.0f); + draw->AddRect(tl, br, IM_COL32(220, 180, 30, static_cast(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, static_cast(alpha * 230))); + draw->AddCircle (ImVec2(iconCx, iconCy), 7.0f, IM_COL32(255, 220, 50, static_cast(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, static_cast(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, static_cast(alpha * 220)), titleStr); + } +} + +// ============================================================ +// Zone Entry Toast +// ============================================================ + +void ToastManager::renderZoneToasts(float deltaTime) { + for (auto& e : zoneToasts_) e.age += deltaTime; + zoneToasts_.erase( + std::remove_if(zoneToasts_.begin(), zoneToasts_.end(), + [](const ZoneToastEntry& e) { return e.age >= kZoneToastLifetime; }), + zoneToasts_.end()); + + // Suppress toasts while the zone text overlay is showing the same zone — + // avoids duplicate "Entering: Stormwind City" messages. + if (zoneTextTimer_ > 0.0f) { + zoneToasts_.erase( + std::remove_if(zoneToasts_.begin(), zoneToasts_.end(), + [this](const ZoneToastEntry& e) { return e.zoneName == zoneTextName_; }), + zoneToasts_.end()); + } + + if (zoneToasts_.empty()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + + ImDrawList* draw = ImGui::GetForegroundDrawList(); + ImFont* font = ImGui::GetFont(); + + for (int i = 0; i < static_cast(zoneToasts_.size()); ++i) { + const auto& e = zoneToasts_[i]; + constexpr float kSlideDur = 0.35f; + float slideIn = std::min(e.age, kSlideDur) / kSlideDur; + float slideOut = std::min(std::max(0.0f, kZoneToastLifetime - e.age), kSlideDur) / kSlideDur; + float slide = std::min(slideIn, slideOut); + float alpha = std::clamp(slide, 0.0f, 1.0f); + + // Measure text to size the toast + ImVec2 nameSz = font->CalcTextSizeA(14.0f, FLT_MAX, 0.0f, e.zoneName.c_str()); + const char* header = "Entering:"; + ImVec2 hdrSz = font->CalcTextSizeA(11.0f, FLT_MAX, 0.0f, header); + + float toastW = std::max(nameSz.x, hdrSz.x) + 28.0f; + float toastH = 42.0f; + + // Center the toast horizontally, appear just below the zone name area (top-center) + float toastX = (screenW - toastW) * 0.5f; + float toastY = 56.0f + i * (toastH + 4.0f); + // Slide down from above + float offY = (1.0f - slide) * (-toastH - 10.0f); + toastY += offY; + + ImVec2 tl(toastX, toastY); + ImVec2 br(toastX + toastW, toastY + toastH); + + draw->AddRectFilled(tl, br, IM_COL32(10, 10, 16, static_cast(alpha * 200)), 6.0f); + draw->AddRect(tl, br, IM_COL32(160, 140, 80, static_cast(alpha * 220)), 6.0f, 0, 1.2f); + + float cx = tl.x + toastW * 0.5f; + draw->AddText(font, 11.0f, + ImVec2(cx - hdrSz.x * 0.5f, tl.y + 5.0f), + IM_COL32(180, 170, 120, static_cast(alpha * 200)), header); + draw->AddText(font, 14.0f, + ImVec2(cx - nameSz.x * 0.5f, tl.y + toastH * 0.5f + 1.0f), + IM_COL32(255, 230, 140, static_cast(alpha * 240)), e.zoneName.c_str()); + } +} + +// ─── Area Trigger Message Toasts ───────────────────────────────────────────── +void ToastManager::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, static_cast(alpha * 190)), 5.0f); + draw->AddRect(tl, br, IM_COL32(100, 160, 220, static_cast(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, static_cast(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, static_cast(alpha * 240)), t.text.c_str()); + } +} + +// ============================================================ +// Level-Up Ding Animation +// ============================================================ + +void ToastManager::triggerDing(uint32_t newLevel, uint32_t hpDelta, uint32_t manaDelta, + uint32_t str, uint32_t agi, uint32_t sta, + uint32_t intel, uint32_t spi) { + // Set golden burst overlay state (consumed by GameScreen) + levelUpFlashAlpha = 1.0f; + levelUpDisplayLevel = newLevel; + + dingTimer_ = DING_DURATION; + dingLevel_ = newLevel; + dingHpDelta_ = hpDelta; + dingManaDelta_ = manaDelta; + dingStats_[0] = str; + dingStats_[1] = agi; + dingStats_[2] = sta; + dingStats_[3] = intel; + dingStats_[4] = spi; + + auto* renderer = core::Application::getInstance().getRenderer(); + if (renderer) { + if (auto* sfx = renderer->getUiSoundManager()) { + sfx->playLevelUp(); + } + renderer->playEmote("cheer"); + } +} + +void ToastManager::renderDingEffect() { + if (dingTimer_ <= 0.0f) return; + + float dt = ImGui::GetIO().DeltaTime; + dingTimer_ -= dt; + if (dingTimer_ < 0.0f) dingTimer_ = 0.0f; + + // Show "You have reached level X!" for the first 2.5s, fade out over last 0.5s. + // The 3D visual effect is handled by Renderer::triggerLevelUpEffect (LevelUp.m2). + constexpr float kFadeTime = 0.5f; + float alpha = dingTimer_ < kFadeTime ? (dingTimer_ / kFadeTime) : 1.0f; + if (alpha <= 0.0f) return; + + ImGuiIO& io = ImGui::GetIO(); + float cx = io.DisplaySize.x * 0.5f; + float cy = io.DisplaySize.y * 0.38f; // Upper-center, like WoW + + ImDrawList* draw = ImGui::GetForegroundDrawList(); + ImFont* font = ImGui::GetFont(); + float baseSize = ImGui::GetFontSize(); + float fontSize = baseSize * 1.8f; + + char buf[64]; + snprintf(buf, sizeof(buf), "You have reached level %u!", dingLevel_); + + ImVec2 sz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, buf); + float tx = cx - sz.x * 0.5f; + float ty = cy - sz.y * 0.5f; + + // Slight black outline for readability + draw->AddText(font, fontSize, ImVec2(tx + 2, ty + 2), + IM_COL32(0, 0, 0, static_cast(alpha * 180)), buf); + // Gold text + draw->AddText(font, fontSize, ImVec2(tx, ty), + IM_COL32(255, 210, 0, static_cast(alpha * 255)), buf); + + // Stat gains below the main text (shown only if server sent deltas) + bool hasStatGains = (dingHpDelta_ > 0 || dingManaDelta_ > 0 || + dingStats_[0] || dingStats_[1] || dingStats_[2] || + dingStats_[3] || dingStats_[4]); + if (hasStatGains) { + float smallSize = baseSize * 0.95f; + float yOff = ty + sz.y + 6.0f; + + // Build stat delta string: "+150 HP +80 Mana +2 Str +2 Agi ..." + static constexpr const char* kStatLabels[] = { "Str", "Agi", "Sta", "Int", "Spi" }; + char statBuf[128]; + int written = 0; + if (dingHpDelta_ > 0) + written += snprintf(statBuf + written, sizeof(statBuf) - written, + "+%u HP ", dingHpDelta_); + if (dingManaDelta_ > 0) + written += snprintf(statBuf + written, sizeof(statBuf) - written, + "+%u Mana ", dingManaDelta_); + for (int i = 0; i < 5 && written < static_cast(sizeof(statBuf)) - 1; ++i) { + if (dingStats_[i] > 0) + written += snprintf(statBuf + written, sizeof(statBuf) - written, + "+%u %s ", dingStats_[i], kStatLabels[i]); + } + // Trim trailing spaces + while (written > 0 && statBuf[written - 1] == ' ') --written; + statBuf[written] = '\0'; + + if (written > 0) { + ImVec2 ssz = font->CalcTextSizeA(smallSize, FLT_MAX, 0.0f, statBuf); + float stx = cx - ssz.x * 0.5f; + draw->AddText(font, smallSize, ImVec2(stx + 1, yOff + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 160)), statBuf); + draw->AddText(font, smallSize, ImVec2(stx, yOff), + IM_COL32(100, 220, 100, static_cast(alpha * 230)), statBuf); + } + } +} + +void ToastManager::triggerAchievementToast(uint32_t achievementId, std::string name) { + achievementToastId_ = achievementId; + achievementToastName_ = std::move(name); + achievementToastTimer_ = ACHIEVEMENT_TOAST_DURATION; + + // Play a UI sound if available + auto* renderer = core::Application::getInstance().getRenderer(); + if (renderer) { + if (auto* sfx = renderer->getUiSoundManager()) { + sfx->playAchievementAlert(); + } + } +} + +void ToastManager::renderAchievementToast() { + if (achievementToastTimer_ <= 0.0f) return; + + float dt = ImGui::GetIO().DeltaTime; + achievementToastTimer_ -= dt; + if (achievementToastTimer_ < 0.0f) achievementToastTimer_ = 0.0f; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + // Slide in from the right — fully visible for most of the duration, slides out at end + constexpr float SLIDE_TIME = 0.4f; + float slideIn = std::min(achievementToastTimer_, ACHIEVEMENT_TOAST_DURATION - achievementToastTimer_); + float slideFrac = (ACHIEVEMENT_TOAST_DURATION > 0.0f && SLIDE_TIME > 0.0f) + ? std::min(slideIn / SLIDE_TIME, 1.0f) + : 1.0f; + + constexpr float TOAST_W = 280.0f; + constexpr float TOAST_H = 60.0f; + float xFull = screenW - TOAST_W - 20.0f; + float xHidden = screenW + 10.0f; + float toastX = xHidden + (xFull - xHidden) * slideFrac; + float toastY = screenH - TOAST_H - 80.0f; // above action bar area + + float alpha = std::min(1.0f, achievementToastTimer_ / 0.5f); // fade at very end + + ImDrawList* draw = ImGui::GetForegroundDrawList(); + + // Background panel (gold border, dark fill) + ImVec2 tl(toastX, toastY); + ImVec2 br(toastX + TOAST_W, toastY + TOAST_H); + draw->AddRectFilled(tl, br, IM_COL32(30, 20, 10, static_cast(alpha * 230)), 6.0f); + draw->AddRect(tl, br, IM_COL32(200, 170, 50, static_cast(alpha * 255)), 6.0f, 0, 2.0f); + + // Title + ImFont* font = ImGui::GetFont(); + float titleSize = 14.0f; + float bodySize = 12.0f; + const char* title = "Achievement Earned!"; + float titleW = font->CalcTextSizeA(titleSize, FLT_MAX, 0.0f, title).x; + float titleX = toastX + (TOAST_W - titleW) * 0.5f; + draw->AddText(font, titleSize, ImVec2(titleX + 1, toastY + 8 + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 180)), title); + draw->AddText(font, titleSize, ImVec2(titleX, toastY + 8), + IM_COL32(255, 215, 0, static_cast(alpha * 255)), title); + + // Achievement name (falls back to ID if name not available) + char idBuf[256]; + const char* achText = achievementToastName_.empty() + ? nullptr : achievementToastName_.c_str(); + if (achText) { + std::snprintf(idBuf, sizeof(idBuf), "%s", achText); + } else { + std::snprintf(idBuf, sizeof(idBuf), "Achievement #%u", achievementToastId_); + } + float idW = font->CalcTextSizeA(bodySize, FLT_MAX, 0.0f, idBuf).x; + float idX = toastX + (TOAST_W - idW) * 0.5f; + draw->AddText(font, bodySize, ImVec2(idX, toastY + 28), + IM_COL32(220, 200, 150, static_cast(alpha * 255)), idBuf); +} + +// --------------------------------------------------------------------------- +// Area discovery toast — "Discovered: ! (+XP XP)" centered on screen +// --------------------------------------------------------------------------- + +void ToastManager::renderDiscoveryToast() { + if (discoveryToastTimer_ <= 0.0f) return; + + float dt = ImGui::GetIO().DeltaTime; + discoveryToastTimer_ -= dt; + if (discoveryToastTimer_ < 0.0f) discoveryToastTimer_ = 0.0f; + + // Fade: ramp up in first 0.4s, hold, fade out in last 1.0s + float alpha; + if (discoveryToastTimer_ > DISCOVERY_TOAST_DURATION - 0.4f) + alpha = 1.0f - (discoveryToastTimer_ - (DISCOVERY_TOAST_DURATION - 0.4f)) / 0.4f; + else if (discoveryToastTimer_ < 1.0f) + alpha = discoveryToastTimer_; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + ImFont* font = ImGui::GetFont(); + ImDrawList* draw = ImGui::GetForegroundDrawList(); + + const char* header = "Discovered!"; + float headerSize = 16.0f; + float nameSize = 28.0f; + float xpSize = 14.0f; + + ImVec2 headerDim = font->CalcTextSizeA(headerSize, FLT_MAX, 0.0f, header); + ImVec2 nameDim = font->CalcTextSizeA(nameSize, FLT_MAX, 0.0f, discoveryToastName_.c_str()); + + char xpBuf[48]; + if (discoveryToastXP_ > 0) + snprintf(xpBuf, sizeof(xpBuf), "+%u XP", discoveryToastXP_); + else + xpBuf[0] = '\0'; + ImVec2 xpDim = font->CalcTextSizeA(xpSize, FLT_MAX, 0.0f, xpBuf); + + // Position slightly below zone text (at 37% down screen) + float centreY = screenH * 0.37f; + float headerX = (screenW - headerDim.x) * 0.5f; + float nameX = (screenW - nameDim.x) * 0.5f; + float xpX = (screenW - xpDim.x) * 0.5f; + float headerY = centreY; + float nameY = centreY + headerDim.y + 4.0f; + float xpY = nameY + nameDim.y + 4.0f; + + // "Discovered!" in gold + draw->AddText(font, headerSize, ImVec2(headerX + 1, headerY + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 160)), header); + draw->AddText(font, headerSize, ImVec2(headerX, headerY), + IM_COL32(255, 215, 0, static_cast(alpha * 255)), header); + + // Area name in white + draw->AddText(font, nameSize, ImVec2(nameX + 1, nameY + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 160)), discoveryToastName_.c_str()); + draw->AddText(font, nameSize, ImVec2(nameX, nameY), + IM_COL32(255, 255, 255, static_cast(alpha * 255)), discoveryToastName_.c_str()); + + // XP gain in light green (if any) + if (xpBuf[0] != '\0') { + draw->AddText(font, xpSize, ImVec2(xpX + 1, xpY + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 140)), xpBuf); + draw->AddText(font, xpSize, ImVec2(xpX, xpY), + IM_COL32(100, 220, 100, static_cast(alpha * 230)), xpBuf); + } +} + +// --------------------------------------------------------------------------- +// Quest objective progress toasts — shown at screen bottom-right on kill/item updates +// --------------------------------------------------------------------------- + +void ToastManager::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); + } +} + +// --------------------------------------------------------------------------- +// Item loot toasts — quality-coloured strip at bottom-left when item received +// --------------------------------------------------------------------------- + +void ToastManager::renderItemLootToasts() { + if (itemLootToasts_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + for (auto& t : itemLootToasts_) t.age += dt; + itemLootToasts_.erase( + std::remove_if(itemLootToasts_.begin(), itemLootToasts_.end(), + [](const ItemLootToastEntry& t) { return t.age >= ITEM_LOOT_TOAST_DURATION; }), + itemLootToasts_.end()); + if (itemLootToasts_.empty()) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + // Quality colours (matching WoW convention) + static const ImU32 kQualityColors[] = { + IM_COL32(157, 157, 157, 255), // 0 grey (poor) + IM_COL32(255, 255, 255, 255), // 1 white (common) + IM_COL32( 30, 255, 30, 255), // 2 green (uncommon) + IM_COL32( 0, 112, 221, 255), // 3 blue (rare) + IM_COL32(163, 53, 238, 255), // 4 purple (epic) + IM_COL32(255, 128, 0, 255), // 5 orange (legendary) + IM_COL32(230, 204, 128, 255), // 6 light gold (artifact) + IM_COL32(230, 204, 128, 255), // 7 light gold (heirloom) + }; + + // Stack at bottom-left above action bars; each item is 24 px tall + constexpr float TOAST_W = 260.0f; + constexpr float TOAST_H = 24.0f; + constexpr float TOAST_GAP = 2.0f; + constexpr float TOAST_X = 14.0f; + float baseY = screenH * 0.68f; // slightly above the whisper toasts + + ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); + const int count = static_cast(itemLootToasts_.size()); + + for (int i = 0; i < count; ++i) { + const auto& toast = itemLootToasts_[i]; + + float remaining = ITEM_LOOT_TOAST_DURATION - toast.age; + float alpha; + if (toast.age < 0.15f) + alpha = toast.age / 0.15f; + else if (remaining < 0.7f) + alpha = remaining / 0.7f; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + // Slide-in from left + float slideX = (toast.age < 0.15f) ? (TOAST_W * (1.0f - toast.age / 0.15f)) : 0.0f; + float tx = TOAST_X - slideX; + float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP); + + uint8_t bgA = static_cast(180 * alpha); + uint8_t fgA = static_cast(255 * alpha); + + // Background: very dark with quality-tinted left border accent + bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H), + IM_COL32(12, 12, 12, bgA), 3.0f); + + // Quality colour accent bar on left edge (3px wide) + ImU32 qualCol = kQualityColors[std::min(static_cast(7u), toast.quality)]; + ImU32 qualColA = (qualCol & 0x00FFFFFFu) | (static_cast(fgA) << 24u); + bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + 3.0f, ty + TOAST_H), qualColA, 3.0f); + + // "Loot:" label in dim white + bgDL->AddText(ImVec2(tx + 7.0f, ty + 5.0f), + IM_COL32(160, 160, 160, static_cast(200 * alpha)), "Loot:"); + + // Item name in quality colour + std::string displayName = toast.name.empty() ? ("Item #" + std::to_string(toast.itemId)) : toast.name; + if (displayName.size() > 26) { displayName.resize(23); displayName += "..."; } + bgDL->AddText(ImVec2(tx + 42.0f, ty + 5.0f), qualColA, displayName.c_str()); + + // Count (if > 1) + if (toast.count > 1) { + char countBuf[12]; + snprintf(countBuf, sizeof(countBuf), "x%u", toast.count); + bgDL->AddText(ImVec2(tx + TOAST_W - 34.0f, ty + 5.0f), + IM_COL32(200, 200, 200, static_cast(200 * alpha)), countBuf); + } + } +} + +// --------------------------------------------------------------------------- +// PvP honor credit toasts — shown at screen top-right on honorable kill +// --------------------------------------------------------------------------- + +void ToastManager::renderPvpHonorToasts() { + if (pvpHonorToasts_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + for (auto& t : pvpHonorToasts_) t.age += dt; + pvpHonorToasts_.erase( + std::remove_if(pvpHonorToasts_.begin(), pvpHonorToasts_.end(), + [](const PvpHonorToastEntry& t) { return t.age >= PVP_HONOR_TOAST_DURATION; }), + pvpHonorToasts_.end()); + if (pvpHonorToasts_.empty()) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; + + // Stack toasts at top-right, below any minimap area + constexpr float TOAST_W = 180.0f; + constexpr float TOAST_H = 30.0f; + constexpr float TOAST_GAP = 3.0f; + constexpr float TOAST_TOP = 10.0f; + float toastX = screenW - TOAST_W - 10.0f; + + ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); + const int count = static_cast(pvpHonorToasts_.size()); + + for (int i = 0; i < count; ++i) { + const auto& toast = pvpHonorToasts_[i]; + + float remaining = PVP_HONOR_TOAST_DURATION - toast.age; + float alpha; + if (toast.age < 0.15f) + alpha = toast.age / 0.15f; + else if (remaining < 0.8f) + alpha = remaining / 0.8f; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + float ty = TOAST_TOP + i * (TOAST_H + TOAST_GAP); + + uint8_t bgA = static_cast(190 * alpha); + uint8_t fgA = static_cast(255 * alpha); + + // Background: dark red (PvP theme) + bgDL->AddRectFilled(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(28, 5, 5, bgA), 4.0f); + bgDL->AddRect(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(200, 50, 50, static_cast(160 * alpha)), 4.0f, 0, 1.2f); + + // Sword ⚔ icon (U+2694, UTF-8: e2 9a 94) + bgDL->AddText(ImVec2(toastX + 7.0f, ty + 7.0f), + IM_COL32(220, 80, 80, fgA), "\xe2\x9a\x94"); + + // "+N Honor" text in gold + char buf[40]; + snprintf(buf, sizeof(buf), "+%u Honor", toast.honor); + bgDL->AddText(ImVec2(toastX + 24.0f, ty + 8.0f), + IM_COL32(255, 210, 50, fgA), buf); + } +} + +// --------------------------------------------------------------------------- +// Nearby player level-up toasts — shown at screen bottom-centre +// --------------------------------------------------------------------------- + +void ToastManager::renderPlayerLevelUpToasts(game::GameHandler& gameHandler) { + if (playerLevelUpToasts_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + for (auto& t : playerLevelUpToasts_) { + t.age += dt; + // Lazy name resolution — fill in once the name cache has it + if (t.playerName.empty() && t.guid != 0) { + t.playerName = gameHandler.lookupName(t.guid); + } + } + playerLevelUpToasts_.erase( + std::remove_if(playerLevelUpToasts_.begin(), playerLevelUpToasts_.end(), + [](const PlayerLevelUpToastEntry& t) { + return t.age >= PLAYER_LEVELUP_TOAST_DURATION; + }), + playerLevelUpToasts_.end()); + if (playerLevelUpToasts_.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 toasts at screen bottom-centre, above action bars + constexpr float TOAST_W = 230.0f; + constexpr float TOAST_H = 38.0f; + constexpr float TOAST_GAP = 4.0f; + float baseY = screenH * 0.72f; + float toastX = (screenW - TOAST_W) * 0.5f; + + ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); + const int count = static_cast(playerLevelUpToasts_.size()); + + for (int i = 0; i < count; ++i) { + const auto& toast = playerLevelUpToasts_[i]; + + float remaining = PLAYER_LEVELUP_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); + + // Subtle pop-up from below during first 0.2s + float slideY = (toast.age < 0.2f) ? (TOAST_H * (1.0f - toast.age / 0.2f)) : 0.0f; + float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP) + slideY; + + uint8_t bgA = static_cast(200 * alpha); + uint8_t fgA = static_cast(255 * alpha); + + // Background: dark gold tint + bgDL->AddRectFilled(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(30, 22, 5, bgA), 5.0f); + // Gold border with glow at peak + float glowStr = (toast.age < 0.5f) ? (1.0f - toast.age / 0.5f) : 0.0f; + uint8_t borderA = static_cast((160 + 80 * glowStr) * alpha); + bgDL->AddRect(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(255, 210, 50, borderA), 5.0f, 0, 1.5f + glowStr * 1.5f); + + // Star ★ icon on left + bgDL->AddText(ImVec2(toastX + 8.0f, ty + 10.0f), + IM_COL32(255, 220, 60, fgA), "\xe2\x98\x85"); // UTF-8 ★ + + // " is now level X!" text + const char* displayName = toast.playerName.empty() ? "A player" : toast.playerName.c_str(); + char buf[64]; + snprintf(buf, sizeof(buf), "%.18s is now level %u!", displayName, toast.newLevel); + bgDL->AddText(ImVec2(toastX + 26.0f, ty + 11.0f), + IM_COL32(255, 230, 100, fgA), buf); + } +} + +// --------------------------------------------------------------------------- +// Resurrection flash — brief screen brightening + "You have been resurrected!" +// banner when the player transitions from ghost back to alive. +// --------------------------------------------------------------------------- + +void ToastManager::renderResurrectFlash() { + if (resurrectFlashTimer_ <= 0.0f) return; + + float dt = ImGui::GetIO().DeltaTime; + resurrectFlashTimer_ -= dt; + if (resurrectFlashTimer_ <= 0.0f) { + resurrectFlashTimer_ = 0.0f; + 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; + + // Normalised age in [0, 1] (0 = just fired, 1 = fully elapsed) + float t = 1.0f - resurrectFlashTimer_ / kResurrectFlashDuration; + + // Alpha envelope: fast fade-in (first 0.15s), hold, then fade-out (last 0.8s) + float alpha; + const float fadeIn = 0.15f / kResurrectFlashDuration; // ~5% of lifetime + const float fadeOut = 0.8f / kResurrectFlashDuration; // ~27% of lifetime + if (t < fadeIn) + alpha = t / fadeIn; + else if (t < 1.0f - fadeOut) + alpha = 1.0f; + else + alpha = (1.0f - t) / fadeOut; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + ImDrawList* bg = ImGui::GetBackgroundDrawList(); + + // Soft golden/white vignette — brightening instead of darkening + uint8_t vigA = static_cast(50 * alpha); + bg->AddRectFilled(ImVec2(0, 0), ImVec2(screenW, screenH), + IM_COL32(200, 230, 255, vigA)); + + // Centered banner panel + constexpr float PANEL_W = 360.0f; + constexpr float PANEL_H = 52.0f; + float px = (screenW - PANEL_W) * 0.5f; + float py = screenH * 0.34f; + + uint8_t bgA = static_cast(210 * alpha); + uint8_t borderA = static_cast(255 * alpha); + uint8_t textA = static_cast(255 * alpha); + + // Background: deep blue-black + bg->AddRectFilled(ImVec2(px, py), ImVec2(px + PANEL_W, py + PANEL_H), + IM_COL32(10, 18, 40, bgA), 8.0f); + + // Border glow: bright holy gold + bg->AddRect(ImVec2(px, py), ImVec2(px + PANEL_W, py + PANEL_H), + IM_COL32(200, 230, 100, borderA), 8.0f, 0, 2.0f); + // Inner halo line + bg->AddRect(ImVec2(px + 3.0f, py + 3.0f), ImVec2(px + PANEL_W - 3.0f, py + PANEL_H - 3.0f), + IM_COL32(255, 255, 180, static_cast(80 * alpha)), 6.0f, 0, 1.0f); + + // "✦ You have been resurrected! ✦" centered + // UTF-8 heavy four-pointed star U+2726: \xe2\x9c\xa6 + const char* banner = "\xe2\x9c\xa6 You have been resurrected! \xe2\x9c\xa6"; + ImFont* font = ImGui::GetFont(); + float fontSize = ImGui::GetFontSize(); + ImVec2 textSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, banner); + float tx = px + (PANEL_W - textSz.x) * 0.5f; + float ty = py + (PANEL_H - textSz.y) * 0.5f; + + // Drop shadow + bg->AddText(font, fontSize, ImVec2(tx + 1.0f, ty + 1.0f), + IM_COL32(0, 0, 0, static_cast(180 * alpha)), banner); + // Main text in warm gold + bg->AddText(font, fontSize, ImVec2(tx, ty), + IM_COL32(255, 240, 120, textA), banner); +} + +// --------------------------------------------------------------------------- +// Whisper toast notifications — brief overlay when a player whispers you +// --------------------------------------------------------------------------- + +void ToastManager::renderWhisperToasts() { + if (whisperToasts_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + + // Age and prune expired toasts + for (auto& t : whisperToasts_) t.age += dt; + whisperToasts_.erase( + std::remove_if(whisperToasts_.begin(), whisperToasts_.end(), + [](const WhisperToastEntry& t) { return t.age >= WHISPER_TOAST_DURATION; }), + whisperToasts_.end()); + if (whisperToasts_.empty()) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + // Stack toasts at bottom-left, above the action bars (y ≈ screenH * 0.72) + // Each toast is ~56px tall with a 4px gap between them. + constexpr float TOAST_W = 280.0f; + constexpr float TOAST_H = 56.0f; + constexpr float TOAST_GAP = 4.0f; + constexpr float TOAST_X = 14.0f; // left edge (won't cover action bars) + float baseY = screenH * 0.72f; + + ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); + + const int count = static_cast(whisperToasts_.size()); + for (int i = 0; i < count; ++i) { + auto& toast = whisperToasts_[i]; + + // Fade in over 0.25s; fade out in last 1.0s + float alpha; + float remaining = WHISPER_TOAST_DURATION - toast.age; + if (toast.age < 0.25f) + alpha = toast.age / 0.25f; + else if (remaining < 1.0f) + alpha = remaining; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + // Slide-in from left: offset 0→0 after 0.25s + float slideX = (toast.age < 0.25f) ? (TOAST_W * (1.0f - toast.age / 0.25f)) : 0.0f; + float tx = TOAST_X - slideX; + float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP); + + uint8_t bgA = static_cast(210 * alpha); + uint8_t fgA = static_cast(255 * alpha); + + // Background panel — dark purple tint (whisper color convention) + bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H), + IM_COL32(25, 10, 40, bgA), 6.0f); + // Purple border + bgDL->AddRect(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H), + IM_COL32(160, 80, 220, static_cast(180 * alpha)), 6.0f, 0, 1.5f); + + // "Whisper" label (small, purple-ish) + bgDL->AddText(ImVec2(tx + 10.0f, ty + 6.0f), + IM_COL32(190, 110, 255, fgA), "Whisper from:"); + + // Sender name (gold) + bgDL->AddText(ImVec2(tx + 10.0f, ty + 20.0f), + IM_COL32(255, 210, 50, fgA), toast.sender.c_str()); + + // Message preview (white, dimmer) + bgDL->AddText(ImVec2(tx + 10.0f, ty + 36.0f), + IM_COL32(220, 220, 220, static_cast(200 * alpha)), + toast.preview.c_str()); + } +} + +// Zone discovery text — "Entering: " fades in/out at screen centre +// --------------------------------------------------------------------------- + +void ToastManager::renderZoneText(game::GameHandler& gameHandler) { + // Poll worldStateZoneId for server-driven zone changes (fires on every zone crossing, + // including sub-zones like Ironforge within Dun Morogh). + uint32_t wsZoneId = gameHandler.getWorldStateZoneId(); + if (wsZoneId != 0 && wsZoneId != lastKnownWorldStateZoneId_) { + lastKnownWorldStateZoneId_ = wsZoneId; + std::string wsName = gameHandler.getWhoAreaName(wsZoneId); + if (!wsName.empty()) { + zoneTextName_ = wsName; + zoneTextTimer_ = ZONE_TEXT_DURATION; + } + } + + // Also poll the renderer for zone name changes (covers map-level transitions + // where worldStateZoneId may not change immediately). + auto* appRenderer = core::Application::getInstance().getRenderer(); + if (appRenderer) { + const std::string& zoneName = appRenderer->getCurrentZoneName(); + if (!zoneName.empty() && zoneName != lastKnownZoneName_) { + lastKnownZoneName_ = zoneName; + // Only override if the worldState hasn't already queued this zone + if (zoneTextName_ != zoneName) { + zoneTextName_ = zoneName; + zoneTextTimer_ = ZONE_TEXT_DURATION; + } + } + } + + if (zoneTextTimer_ <= 0.0f || zoneTextName_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + zoneTextTimer_ -= dt; + if (zoneTextTimer_ < 0.0f) zoneTextTimer_ = 0.0f; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + // Fade: ramp up in first 0.5 s, hold, fade out in last 1.0 s + float alpha; + if (zoneTextTimer_ > ZONE_TEXT_DURATION - 0.5f) + alpha = 1.0f - (zoneTextTimer_ - (ZONE_TEXT_DURATION - 0.5f)) / 0.5f; + else if (zoneTextTimer_ < 1.0f) + alpha = zoneTextTimer_; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + ImFont* font = ImGui::GetFont(); + + // "Entering:" header + const char* header = "Entering:"; + float headerSize = 16.0f; + float nameSize = 26.0f; + + ImVec2 headerDim = font->CalcTextSizeA(headerSize, FLT_MAX, 0.0f, header); + ImVec2 nameDim = font->CalcTextSizeA(nameSize, FLT_MAX, 0.0f, zoneTextName_.c_str()); + + float centreY = screenH * 0.30f; // upper third, like WoW + float headerX = (screenW - headerDim.x) * 0.5f; + float nameX = (screenW - nameDim.x) * 0.5f; + float headerY = centreY; + float nameY = centreY + headerDim.y + 4.0f; + + ImDrawList* draw = ImGui::GetForegroundDrawList(); + + // "Entering:" in gold + draw->AddText(font, headerSize, ImVec2(headerX + 1, headerY + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 160)), header); + draw->AddText(font, headerSize, ImVec2(headerX, headerY), + IM_COL32(255, 215, 0, static_cast(alpha * 255)), header); + + // Zone name in white + draw->AddText(font, nameSize, ImVec2(nameX + 1, nameY + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 160)), zoneTextName_.c_str()); + draw->AddText(font, nameSize, ImVec2(nameX, nameY), + IM_COL32(255, 255, 255, static_cast(alpha * 255)), zoneTextName_.c_str()); +} + +} } // namespace wowee::ui