Add UIErrorsFrame: center-bottom spell error overlay with fade-out

This commit is contained in:
Kelsi 2026-03-12 01:15:11 -07:00
parent 955b22841e
commit 25e2c60603
4 changed files with 92 additions and 2 deletions

View file

@ -1269,6 +1269,11 @@ public:
using PlayPositionalSoundCallback = std::function<void(uint32_t soundId, uint64_t sourceGuid)>;
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(const std::string& msg)>;
void setUIErrorCallback(UIErrorCallback cb) { uiErrorCallback_ = std::move(cb); }
void addUIError(const std::string& msg) { if (uiErrorCallback_) uiErrorCallback_(msg); }
// Mount state
using MountCallback = std::function<void(uint32_t mountDisplayId)>; // 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

View file

@ -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<UIErrorEntry> 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);

View file

@ -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<int>(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:

View file

@ -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<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(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<int>(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<uint8_t>(alpha * 255.0f);
ImU32 textCol = IM_COL32(255, 50, 50, a8);
ImU32 shadowCol= IM_COL32( 0, 0, 0, static_cast<uint8_t>(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
// ============================================================