diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 2001e4eb..84a3170d 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1269,6 +1269,11 @@ public: using PlayPositionalSoundCallback = std::function; void setPlayPositionalSoundCallback(PlayPositionalSoundCallback cb) { playPositionalSoundCallback_ = std::move(cb); } + // UI error frame: prominent on-screen error messages (spell can't be cast, etc.) + using UIErrorCallback = std::function; + void setUIErrorCallback(UIErrorCallback cb) { uiErrorCallback_ = std::move(cb); } + void addUIError(const std::string& msg) { if (uiErrorCallback_) uiErrorCallback_(msg); } + // Mount state using MountCallback = std::function; // 0 = dismount void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); } @@ -2548,6 +2553,9 @@ private: PlayMusicCallback playMusicCallback_; PlaySoundCallback playSoundCallback_; PlayPositionalSoundCallback playPositionalSoundCallback_; + + // ---- UI error frame callback ---- + UIErrorCallback uiErrorCallback_; }; } // namespace game diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index a27f1808..e2640ff6 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -76,6 +76,12 @@ private: float damageFlashAlpha_ = 0.0f; // Screen edge flash intensity (fades to 0) float levelUpFlashAlpha_ = 0.0f; // Golden level-up burst effect (fades to 0) uint32_t levelUpDisplayLevel_ = 0; // Level shown in level-up text + + // UIErrorsFrame: WoW-style center-bottom error messages (spell fails, out of range, etc.) + struct UIErrorEntry { std::string text; float age = 0.0f; }; + std::vector uiErrors_; + bool uiErrorCallbackSet_ = false; + static constexpr float kUIErrorLifetime = 2.5f; bool showPlayerInfo = false; bool showSocialFrame_ = false; // O key toggles social/friends list bool showGuildRoster_ = false; @@ -256,6 +262,7 @@ private: void renderCombatText(game::GameHandler& gameHandler); void renderPartyFrames(game::GameHandler& gameHandler); void renderBossFrames(game::GameHandler& gameHandler); + void renderUIErrors(game::GameHandler& gameHandler, float deltaTime); void renderGroupInvitePopup(game::GameHandler& gameHandler); void renderDuelRequestPopup(game::GameHandler& gameHandler); void renderLootRollPopup(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 47d1f2ca..97b08e22 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1124,6 +1124,7 @@ void GameHandler::update(float deltaTime) { autoAttackOutOfRangeTime_ += deltaTime; if (autoAttackRangeWarnCooldown_ <= 0.0f) { addSystemChatMessage("Target is too far away."); + addUIError("Target is too far away."); autoAttackRangeWarnCooldown_ = 1.25f; } // Stop chasing stale swings when the target remains out of range. @@ -1959,11 +1960,13 @@ void GameHandler::handlePacket(network::Packet& packet) { playerPowerType = static_cast(pu->getPowerType()); } const char* reason = getSpellCastResultString(castResult, playerPowerType); + std::string errMsg = reason ? reason + : ("Spell cast failed (error " + std::to_string(castResult) + ")"); + addUIError(errMsg); MessageChatData msg; msg.type = ChatType::SYSTEM; msg.language = ChatLanguage::UNIVERSAL; - msg.message = reason ? reason - : ("Spell cast failed (error " + std::to_string(castResult) + ")"); + msg.message = errMsg; addLocalChatMessage(msg); } } @@ -2274,6 +2277,7 @@ void GameHandler::handlePacket(network::Packet& packet) { LOG_INFO("SMSG_ENABLE_BARBER_SHOP: barber shop available"); break; case Opcode::SMSG_FEIGN_DEATH_RESISTED: + addUIError("Your Feign Death was resisted."); addSystemChatMessage("Your Feign Death attempt was resisted."); LOG_DEBUG("SMSG_FEIGN_DEATH_RESISTED"); break; @@ -3173,6 +3177,7 @@ void GameHandler::handlePacket(network::Packet& packet) { handleDuelWinner(packet); break; case Opcode::SMSG_DUEL_OUTOFBOUNDS: + addUIError("You are out of the duel area!"); addSystemChatMessage("You are out of the duel area!"); break; case Opcode::SMSG_DUEL_INBOUNDS: diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 5acec3f6..ce7dbe05 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -227,6 +227,15 @@ void GameScreen::render(game::GameHandler& gameHandler) { achievementCallbackSet_ = true; } + // Set up UI error frame callback (once) + if (!uiErrorCallbackSet_) { + gameHandler.setUIErrorCallback([this](const std::string& msg) { + uiErrors_.push_back({msg, 0.0f}); + if (uiErrors_.size() > 5) uiErrors_.erase(uiErrors_.begin()); + }); + uiErrorCallbackSet_ = true; + } + // Apply UI transparency setting float prevAlpha = ImGui::GetStyle().Alpha; ImGui::GetStyle().Alpha = uiOpacity_; @@ -443,6 +452,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderNameplates(gameHandler); // player names always shown; NPC plates gated by showNameplates_ renderBattlegroundScore(gameHandler); renderCombatText(gameHandler); + renderUIErrors(gameHandler, ImGui::GetIO().DeltaTime); if (showRaidFrames_) { renderPartyFrames(gameHandler); } @@ -6515,6 +6525,66 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } +// ============================================================ +// UI Error Frame (WoW-style center-bottom error overlay) +// ============================================================ + +void GameScreen::renderUIErrors(game::GameHandler& /*gameHandler*/, float deltaTime) { + // Age out old entries + for (auto& e : uiErrors_) e.age += deltaTime; + uiErrors_.erase( + std::remove_if(uiErrors_.begin(), uiErrors_.end(), + [](const UIErrorEntry& e) { return e.age >= kUIErrorLifetime; }), + uiErrors_.end()); + + if (uiErrors_.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; + + // Fixed invisible overlay + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(screenW, screenH)); + ImGuiWindowFlags flags = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + if (ImGui::Begin("##UIErrors", nullptr, flags)) { + // Render messages stacked above the action bar (~200px from bottom) + // The newest message is on top; older ones fade below it. + const float baseY = screenH - 200.0f; + const float lineH = 20.0f; + const int count = static_cast(uiErrors_.size()); + + ImDrawList* draw = ImGui::GetWindowDrawList(); + for (int i = count - 1; i >= 0; --i) { + const auto& e = uiErrors_[i]; + float alpha = 1.0f - (e.age / kUIErrorLifetime); + alpha = std::max(0.0f, std::min(1.0f, alpha)); + + // Fade fast in the last 0.5 s + if (e.age > kUIErrorLifetime - 0.5f) + alpha *= (kUIErrorLifetime - e.age) / 0.5f; + + uint8_t a8 = static_cast(alpha * 255.0f); + ImU32 textCol = IM_COL32(255, 50, 50, a8); + ImU32 shadowCol= IM_COL32( 0, 0, 0, static_cast(alpha * 180)); + + const char* txt = e.text.c_str(); + ImVec2 sz = ImGui::CalcTextSize(txt); + float x = std::round((screenW - sz.x) * 0.5f); + float y = std::round(baseY - (count - 1 - i) * lineH); + + // Drop shadow + draw->AddText(ImVec2(x + 1, y + 1), shadowCol, txt); + draw->AddText(ImVec2(x, y), textCol, txt); + } + } + ImGui::End(); + ImGui::PopStyleVar(); +} + // ============================================================ // Boss Encounter Frames // ============================================================