mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Handle SMSG_ACHIEVEMENT_EARNED with toast banner and chat notification
- Parse SMSG_ACHIEVEMENT_EARNED (guid + achievementId + PackedTime date) and fire AchievementEarnedCallback for self, chat notify for others - Add renderAchievementToast() to GameScreen: slides in from right, gold-bordered panel with "Achievement Earned!" title + ID, 5s duration with 0.4s slide-in/out animation and fade at end - Add triggerAchievementToast(uint32_t) public method on GameScreen - Wire AchievementEarnedCallback in application.cpp - Add playAchievementAlert() to UiSoundManager, loads Sound\Interface\AchievementSound.wav with level-up fallback - SMSG_ALL_ACHIEVEMENT_DATA silently consumed (no tracker UI yet)
This commit is contained in:
parent
200a00d4f5
commit
e4f53ce0c3
7 changed files with 159 additions and 0 deletions
|
|
@ -67,6 +67,9 @@ public:
|
||||||
// Level up
|
// Level up
|
||||||
void playLevelUp();
|
void playLevelUp();
|
||||||
|
|
||||||
|
// Achievement
|
||||||
|
void playAchievementAlert();
|
||||||
|
|
||||||
// Error/feedback
|
// Error/feedback
|
||||||
void playError();
|
void playError();
|
||||||
void playTargetSelect();
|
void playTargetSelect();
|
||||||
|
|
@ -114,6 +117,7 @@ private:
|
||||||
std::vector<UISample> drinkingSounds_;
|
std::vector<UISample> drinkingSounds_;
|
||||||
|
|
||||||
std::vector<UISample> levelUpSounds_;
|
std::vector<UISample> levelUpSounds_;
|
||||||
|
std::vector<UISample> achievementSounds_;
|
||||||
|
|
||||||
std::vector<UISample> errorSounds_;
|
std::vector<UISample> errorSounds_;
|
||||||
std::vector<UISample> selectTargetSounds_;
|
std::vector<UISample> selectTargetSounds_;
|
||||||
|
|
|
||||||
|
|
@ -834,6 +834,10 @@ public:
|
||||||
using OtherPlayerLevelUpCallback = std::function<void(uint64_t guid, uint32_t newLevel)>;
|
using OtherPlayerLevelUpCallback = std::function<void(uint64_t guid, uint32_t newLevel)>;
|
||||||
void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); }
|
void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); }
|
||||||
|
|
||||||
|
// Achievement earned callback — fires when SMSG_ACHIEVEMENT_EARNED is received
|
||||||
|
using AchievementEarnedCallback = std::function<void(uint32_t achievementId)>;
|
||||||
|
void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); }
|
||||||
|
|
||||||
// Mount state
|
// Mount state
|
||||||
using MountCallback = std::function<void(uint32_t mountDisplayId)>; // 0 = dismount
|
using MountCallback = std::function<void(uint32_t mountDisplayId)>; // 0 = dismount
|
||||||
void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); }
|
void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); }
|
||||||
|
|
@ -1166,6 +1170,7 @@ private:
|
||||||
void handleSpellGo(network::Packet& packet);
|
void handleSpellGo(network::Packet& packet);
|
||||||
void handleSpellCooldown(network::Packet& packet);
|
void handleSpellCooldown(network::Packet& packet);
|
||||||
void handleCooldownEvent(network::Packet& packet);
|
void handleCooldownEvent(network::Packet& packet);
|
||||||
|
void handleAchievementEarned(network::Packet& packet);
|
||||||
void handleAuraUpdate(network::Packet& packet, bool isAll);
|
void handleAuraUpdate(network::Packet& packet, bool isAll);
|
||||||
void handleLearnedSpell(network::Packet& packet);
|
void handleLearnedSpell(network::Packet& packet);
|
||||||
void handleSupercededSpell(network::Packet& packet);
|
void handleSupercededSpell(network::Packet& packet);
|
||||||
|
|
@ -1873,6 +1878,7 @@ private:
|
||||||
ChargeCallback chargeCallback_;
|
ChargeCallback chargeCallback_;
|
||||||
LevelUpCallback levelUpCallback_;
|
LevelUpCallback levelUpCallback_;
|
||||||
OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_;
|
OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_;
|
||||||
|
AchievementEarnedCallback achievementEarnedCallback_;
|
||||||
MountCallback mountCallback_;
|
MountCallback mountCallback_;
|
||||||
TaxiPrecacheCallback taxiPrecacheCallback_;
|
TaxiPrecacheCallback taxiPrecacheCallback_;
|
||||||
TaxiOrientationCallback taxiOrientationCallback_;
|
TaxiOrientationCallback taxiOrientationCallback_;
|
||||||
|
|
|
||||||
|
|
@ -326,8 +326,15 @@ private:
|
||||||
uint32_t dingLevel_ = 0;
|
uint32_t dingLevel_ = 0;
|
||||||
void renderDingEffect();
|
void renderDingEffect();
|
||||||
|
|
||||||
|
// Achievement toast banner
|
||||||
|
static constexpr float ACHIEVEMENT_TOAST_DURATION = 5.0f;
|
||||||
|
float achievementToastTimer_ = 0.0f;
|
||||||
|
uint32_t achievementToastId_ = 0;
|
||||||
|
void renderAchievementToast();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void triggerDing(uint32_t newLevel);
|
void triggerDing(uint32_t newLevel);
|
||||||
|
void triggerAchievementToast(uint32_t achievementId);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,13 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) {
|
||||||
levelUpSounds_.resize(1);
|
levelUpSounds_.resize(1);
|
||||||
bool levelUpLoaded = loadSound("Sound\\Interface\\LevelUp.wav", levelUpSounds_[0], assets);
|
bool levelUpLoaded = loadSound("Sound\\Interface\\LevelUp.wav", levelUpSounds_[0], assets);
|
||||||
|
|
||||||
|
// Load achievement sound (WotLK: Sound\Interface\AchievementSound.wav)
|
||||||
|
achievementSounds_.resize(1);
|
||||||
|
if (!loadSound("Sound\\Interface\\AchievementSound.wav", achievementSounds_[0], assets)) {
|
||||||
|
// Fallback to level-up sound if achievement sound is missing
|
||||||
|
achievementSounds_ = levelUpSounds_;
|
||||||
|
}
|
||||||
|
|
||||||
// Load error/feedback sounds
|
// Load error/feedback sounds
|
||||||
errorSounds_.resize(1);
|
errorSounds_.resize(1);
|
||||||
loadSound("Sound\\Interface\\Error.wav", errorSounds_[0], assets);
|
loadSound("Sound\\Interface\\Error.wav", errorSounds_[0], assets);
|
||||||
|
|
@ -210,6 +217,9 @@ void UiSoundManager::playDrinking() { playSound(drinkingSounds_); }
|
||||||
// Level up
|
// Level up
|
||||||
void UiSoundManager::playLevelUp() { playSound(levelUpSounds_); }
|
void UiSoundManager::playLevelUp() { playSound(levelUpSounds_); }
|
||||||
|
|
||||||
|
// Achievement
|
||||||
|
void UiSoundManager::playAchievementAlert() { playSound(achievementSounds_); }
|
||||||
|
|
||||||
// Error/feedback
|
// Error/feedback
|
||||||
void UiSoundManager::playError() { playSound(errorSounds_); }
|
void UiSoundManager::playError() { playSound(errorSounds_); }
|
||||||
void UiSoundManager::playTargetSelect() { playSound(selectTargetSounds_); }
|
void UiSoundManager::playTargetSelect() { playSound(selectTargetSounds_); }
|
||||||
|
|
|
||||||
|
|
@ -2089,6 +2089,13 @@ void Application::setupUICallbacks() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Achievement earned callback — show toast banner
|
||||||
|
gameHandler->setAchievementEarnedCallback([this](uint32_t achievementId) {
|
||||||
|
if (uiManager) {
|
||||||
|
uiManager->getGameScreen().triggerAchievementToast(achievementId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Other player level-up callback — trigger 3D effect + chat notification
|
// Other player level-up callback — trigger 3D effect + chat notification
|
||||||
gameHandler->setOtherPlayerLevelUpCallback([this](uint64_t guid, uint32_t newLevel) {
|
gameHandler->setOtherPlayerLevelUpCallback([this](uint64_t guid, uint32_t newLevel) {
|
||||||
if (!gameHandler || !renderer) return;
|
if (!gameHandler || !renderer) return;
|
||||||
|
|
|
||||||
|
|
@ -1825,6 +1825,12 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case Opcode::SMSG_ACHIEVEMENT_EARNED:
|
||||||
|
handleAchievementEarned(packet);
|
||||||
|
break;
|
||||||
|
case Opcode::SMSG_ALL_ACHIEVEMENT_DATA:
|
||||||
|
// Initial data burst on login — ignored for now (no achievement tracker UI).
|
||||||
|
break;
|
||||||
case Opcode::SMSG_CANCEL_AUTO_REPEAT:
|
case Opcode::SMSG_CANCEL_AUTO_REPEAT:
|
||||||
break; // Server signals to stop a repeating spell (wand/shoot); no client action needed
|
break; // Server signals to stop a repeating spell (wand/shoot); no client action needed
|
||||||
case Opcode::SMSG_AURA_UPDATE:
|
case Opcode::SMSG_AURA_UPDATE:
|
||||||
|
|
@ -14870,5 +14876,54 @@ void GameHandler::handleAuctionCommandResult(network::Packet& packet) {
|
||||||
" error=", result.errorCode);
|
" error=", result.errorCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SMSG_ACHIEVEMENT_EARNED (WotLK 3.3.5a wire 0x4AB)
|
||||||
|
// uint64 guid — player who earned it (may be another player)
|
||||||
|
// uint32 achievementId — Achievement.dbc ID
|
||||||
|
// PackedTime date — uint32 bitfield (seconds since epoch)
|
||||||
|
// uint32 realmFirst — how many on realm also got it (0 = realm first)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void GameHandler::handleAchievementEarned(network::Packet& packet) {
|
||||||
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
||||||
|
if (remaining < 16) return; // guid(8) + id(4) + date(4)
|
||||||
|
|
||||||
|
uint64_t guid = packet.readUInt64();
|
||||||
|
uint32_t achievementId = packet.readUInt32();
|
||||||
|
/*uint32_t date =*/ packet.readUInt32(); // PackedTime — not displayed
|
||||||
|
|
||||||
|
// Show chat notification
|
||||||
|
bool isSelf = (guid == playerGuid);
|
||||||
|
if (isSelf) {
|
||||||
|
char buf[128];
|
||||||
|
std::snprintf(buf, sizeof(buf),
|
||||||
|
"Achievement earned! (ID %u)", achievementId);
|
||||||
|
addSystemChatMessage(buf);
|
||||||
|
|
||||||
|
if (achievementEarnedCallback_) {
|
||||||
|
achievementEarnedCallback_(achievementId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Another player in the zone earned an achievement
|
||||||
|
std::string senderName;
|
||||||
|
auto entity = entityManager.getEntity(guid);
|
||||||
|
if (auto* unit = dynamic_cast<Unit*>(entity.get())) {
|
||||||
|
senderName = unit->getName();
|
||||||
|
}
|
||||||
|
if (senderName.empty()) {
|
||||||
|
char tmp[32];
|
||||||
|
std::snprintf(tmp, sizeof(tmp), "0x%llX",
|
||||||
|
static_cast<unsigned long long>(guid));
|
||||||
|
senderName = tmp;
|
||||||
|
}
|
||||||
|
char buf[256];
|
||||||
|
std::snprintf(buf, sizeof(buf),
|
||||||
|
"%s has earned an achievement! (ID %u)", senderName.c_str(), achievementId);
|
||||||
|
addSystemChatMessage(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("SMSG_ACHIEVEMENT_EARNED: guid=0x", std::hex, guid, std::dec,
|
||||||
|
" achievementId=", achievementId, " self=", isSelf);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace game
|
} // namespace game
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -421,6 +421,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
renderEscapeMenu();
|
renderEscapeMenu();
|
||||||
renderSettingsWindow();
|
renderSettingsWindow();
|
||||||
renderDingEffect();
|
renderDingEffect();
|
||||||
|
renderAchievementToast();
|
||||||
|
|
||||||
// World map (M key toggle handled inside)
|
// World map (M key toggle handled inside)
|
||||||
renderWorldMap(gameHandler);
|
renderWorldMap(gameHandler);
|
||||||
|
|
@ -8878,6 +8879,75 @@ void GameScreen::renderDingEffect() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScreen::triggerAchievementToast(uint32_t achievementId) {
|
||||||
|
achievementToastId_ = achievementId;
|
||||||
|
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<float>(window->getWidth()) : 1280.0f;
|
||||||
|
float screenH = window ? static_cast<float>(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, (int)(alpha * 230)), 6.0f);
|
||||||
|
draw->AddRect(tl, br, IM_COL32(200, 170, 50, (int)(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, (int)(alpha * 180)), title);
|
||||||
|
draw->AddText(font, titleSize, ImVec2(titleX, toastY + 8),
|
||||||
|
IM_COL32(255, 215, 0, (int)(alpha * 255)), title);
|
||||||
|
|
||||||
|
// Achievement ID line (until we have Achievement.dbc name lookup)
|
||||||
|
char idBuf[64];
|
||||||
|
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, (int)(alpha * 255)), idBuf);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Dungeon Finder window (toggle with hotkey or bag-bar button)
|
// Dungeon Finder window (toggle with hotkey or bag-bar button)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue