diff --git a/include/audio/ui_sound_manager.hpp b/include/audio/ui_sound_manager.hpp index 1ab91ebd..241014ae 100644 --- a/include/audio/ui_sound_manager.hpp +++ b/include/audio/ui_sound_manager.hpp @@ -67,6 +67,9 @@ public: // Level up void playLevelUp(); + // Achievement + void playAchievementAlert(); + // Error/feedback void playError(); void playTargetSelect(); @@ -114,6 +117,7 @@ private: std::vector drinkingSounds_; std::vector levelUpSounds_; + std::vector achievementSounds_; std::vector errorSounds_; std::vector selectTargetSounds_; diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 2790bf00..48f28f52 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -834,6 +834,10 @@ public: using OtherPlayerLevelUpCallback = std::function; void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); } + // Achievement earned callback — fires when SMSG_ACHIEVEMENT_EARNED is received + using AchievementEarnedCallback = std::function; + void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); } + // Mount state using MountCallback = std::function; // 0 = dismount void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); } @@ -1166,6 +1170,7 @@ private: void handleSpellGo(network::Packet& packet); void handleSpellCooldown(network::Packet& packet); void handleCooldownEvent(network::Packet& packet); + void handleAchievementEarned(network::Packet& packet); void handleAuraUpdate(network::Packet& packet, bool isAll); void handleLearnedSpell(network::Packet& packet); void handleSupercededSpell(network::Packet& packet); @@ -1873,6 +1878,7 @@ private: ChargeCallback chargeCallback_; LevelUpCallback levelUpCallback_; OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_; + AchievementEarnedCallback achievementEarnedCallback_; MountCallback mountCallback_; TaxiPrecacheCallback taxiPrecacheCallback_; TaxiOrientationCallback taxiOrientationCallback_; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index d2713a55..f1075d75 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -326,8 +326,15 @@ private: uint32_t dingLevel_ = 0; void renderDingEffect(); + // Achievement toast banner + static constexpr float ACHIEVEMENT_TOAST_DURATION = 5.0f; + float achievementToastTimer_ = 0.0f; + uint32_t achievementToastId_ = 0; + void renderAchievementToast(); + public: void triggerDing(uint32_t newLevel); + void triggerAchievementToast(uint32_t achievementId); }; } // namespace ui diff --git a/src/audio/ui_sound_manager.cpp b/src/audio/ui_sound_manager.cpp index f50f1d6f..f32f0d9b 100644 --- a/src/audio/ui_sound_manager.cpp +++ b/src/audio/ui_sound_manager.cpp @@ -105,6 +105,13 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) { levelUpSounds_.resize(1); 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 errorSounds_.resize(1); loadSound("Sound\\Interface\\Error.wav", errorSounds_[0], assets); @@ -210,6 +217,9 @@ void UiSoundManager::playDrinking() { playSound(drinkingSounds_); } // Level up void UiSoundManager::playLevelUp() { playSound(levelUpSounds_); } +// Achievement +void UiSoundManager::playAchievementAlert() { playSound(achievementSounds_); } + // Error/feedback void UiSoundManager::playError() { playSound(errorSounds_); } void UiSoundManager::playTargetSelect() { playSound(selectTargetSounds_); } diff --git a/src/core/application.cpp b/src/core/application.cpp index 21c6f533..203f2e1b 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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 gameHandler->setOtherPlayerLevelUpCallback([this](uint64_t guid, uint32_t newLevel) { if (!gameHandler || !renderer) return; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index bfc1ef9c..a0afadf1 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1825,6 +1825,12 @@ void GameHandler::handlePacket(network::Packet& packet) { } 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: break; // Server signals to stop a repeating spell (wand/shoot); no client action needed case Opcode::SMSG_AURA_UPDATE: @@ -14870,5 +14876,54 @@ void GameHandler::handleAuctionCommandResult(network::Packet& packet) { " 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(entity.get())) { + senderName = unit->getName(); + } + if (senderName.empty()) { + char tmp[32]; + std::snprintf(tmp, sizeof(tmp), "0x%llX", + static_cast(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 wowee diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 0f275f23..dd1fa6ef 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -421,6 +421,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderEscapeMenu(); renderSettingsWindow(); renderDingEffect(); + renderAchievementToast(); // World map (M key toggle handled inside) 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(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, (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) // ---------------------------------------------------------------------------