feat: show whisper toast notification when a player whispers you

Adds a slide-in toast overlay at the bottom-left of the screen whenever
an incoming whisper arrives. Toasts display "Whisper from:", the sender
name in gold, and a truncated message preview. Up to 3 toasts stack with
a 5s lifetime; each fades in over 0.25s and fades out in the final 1s.
This commit is contained in:
Kelsi 2026-03-12 15:53:45 -07:00
parent 77879769d3
commit 5216582f15
2 changed files with 112 additions and 0 deletions

View file

@ -527,6 +527,17 @@ private:
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<WhisperToastEntry> whisperToasts_;
size_t whisperSeenCount_ = 0; // how many chat entries have been scanned for whispers
void renderWhisperToasts();
// Zone discovery text ("Entering: <ZoneName>")
static constexpr float ZONE_TEXT_DURATION = 5.0f;
float zoneTextTimer_ = 0.0f;

View file

@ -639,6 +639,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
renderDingEffect();
renderAchievementToast();
renderDiscoveryToast();
renderWhisperToasts();
renderZoneText();
// World map (M key toggle handled inside)
@ -1689,6 +1690,32 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
chatMentionSeenCount_ = chatHistory.size(); // reset if history was cleared
}
// Scan NEW messages for incoming whispers and push a toast notification
{
size_t histSize = chatHistory.size();
if (histSize < whisperSeenCount_) whisperSeenCount_ = histSize; // cleared
for (size_t wi = whisperSeenCount_; wi < histSize; ++wi) {
const auto& wMsg = chatHistory[wi];
if (wMsg.type == game::ChatType::WHISPER ||
wMsg.type == game::ChatType::RAID_BOSS_WHISPER) {
WhisperToastEntry toast;
toast.sender = wMsg.senderName;
if (toast.sender.empty() && wMsg.senderGuid != 0)
toast.sender = gameHandler.lookupName(wMsg.senderGuid);
if (toast.sender.empty()) toast.sender = "Unknown";
// Truncate preview to 60 chars
toast.preview = wMsg.message.size() > 60
? wMsg.message.substr(0, 57) + "..."
: wMsg.message;
toast.age = 0.0f;
// Keep at most 3 stacked toasts
if (whisperToasts_.size() >= 3) whisperToasts_.erase(whisperToasts_.begin());
whisperToasts_.push_back(std::move(toast));
}
}
whisperSeenCount_ = histSize;
}
int chatMsgIdx = 0;
for (const auto& msg : chatHistory) {
if (!shouldShowMessage(msg, activeChatTab_)) continue;
@ -18012,6 +18039,80 @@ void GameScreen::renderDiscoveryToast() {
}
// ---------------------------------------------------------------------------
// 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<int>(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<uint8_t>(210 * alpha);
uint8_t fgA = static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(200 * alpha)),
toast.preview.c_str());
}
}
// Zone discovery text — "Entering: <ZoneName>" fades in/out at screen centre
// ---------------------------------------------------------------------------