diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 5e9c969c..27e0715c 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -706,6 +706,10 @@ public: } const std::unordered_map& getNpcQuestStatuses() const { return npcQuestStatus_; } + // Level-up callback — fires when the player gains a level (newLevel > 1) + using LevelUpCallback = std::function; + void setLevelUpCallback(LevelUpCallback cb) { levelUpCallback_ = std::move(cb); } + // Mount state using MountCallback = std::function; // 0 = dismount void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); } @@ -1558,6 +1562,7 @@ private: NpcGreetingCallback npcGreetingCallback_; NpcFarewellCallback npcFarewellCallback_; NpcVendorCallback npcVendorCallback_; + LevelUpCallback levelUpCallback_; MountCallback mountCallback_; TaxiPrecacheCallback taxiPrecacheCallback_; TaxiOrientationCallback taxiOrientationCallback_; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 8dfa0dd4..0a9b056a 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -276,6 +276,15 @@ private: // Left-click targeting: distinguish click from camera drag glm::vec2 leftClickPressPos_ = glm::vec2(0.0f); bool leftClickWasPress_ = false; + + // Level-up ding animation + static constexpr float DING_DURATION = 3.0f; + float dingTimer_ = 0.0f; + uint32_t dingLevel_ = 0; + void renderDingEffect(); + +public: + void triggerDing(uint32_t newLevel); }; } // namespace ui diff --git a/src/core/application.cpp b/src/core/application.cpp index 4d8feec7..3573efba 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1280,6 +1280,13 @@ void Application::setupUICallbacks() { despawnOnlineGameObject(guid); }); + // Level-up callback — play sound, cheer emote, and trigger UI ding overlay + gameHandler->setLevelUpCallback([this](uint32_t newLevel) { + if (uiManager) { + uiManager->getGameScreen().triggerDing(newLevel); + } + }); + // Mount callback (online mode) - defer heavy model load to next frame gameHandler->setMountCallback([this](uint32_t mountDisplayId) { if (mountDisplayId == 0) { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index d163d1c4..597520ce 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3886,6 +3886,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { LOG_INFO("Next level XP updated: ", val); } else if (key == ufPlayerLevel) { + uint32_t oldLevel = serverPlayerLevel_; serverPlayerLevel_ = val; LOG_INFO("Level updated: ", val); for (auto& ch : characters) { @@ -3894,6 +3895,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { break; } } + if (val > oldLevel && oldLevel > 0 && levelUpCallback_) { + levelUpCallback_(val); + } } else if (key == ufCoinage) { playerMoneyCopper_ = val; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index ec4f89ed..6880bc3d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -281,6 +281,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderChatBubbles(gameHandler); renderEscapeMenu(); renderSettingsWindow(); + renderDingEffect(); // World map (M key toggle handled inside) renderWorldMap(gameHandler); @@ -5104,6 +5105,15 @@ void GameScreen::renderEscapeMenu() { showEscapeMenu = false; } + ImGui::Spacing(); + if (ImGui::Button("Test: Level Up", ImVec2(-1, 0))) { + uint32_t lvl = 1; + if (auto* gh = core::Application::getInstance().getGameHandler()) { + lvl = gh->getPlayerLevel(); + } + triggerDing(lvl > 0 ? lvl : 1); + showEscapeMenu = false; + } ImGui::Spacing(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f)); if (ImGui::Button("Back to Game", ImVec2(-1, 0))) { @@ -7151,4 +7161,90 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { if (!open) gameHandler.closeAuctionHouse(); } +// ============================================================ +// Level-Up Ding Animation +// ============================================================ + +void GameScreen::triggerDing(uint32_t newLevel) { + dingTimer_ = DING_DURATION; + dingLevel_ = newLevel; + + auto* renderer = core::Application::getInstance().getRenderer(); + if (renderer) { + if (auto* sfx = renderer->getUiSoundManager()) { + sfx->playLevelUp(); + } + renderer->playEmote("cheer"); + } +} + +void GameScreen::renderDingEffect() { + if (dingTimer_ <= 0.0f) return; + + float dt = ImGui::GetIO().DeltaTime; + dingTimer_ -= dt; + if (dingTimer_ < 0.0f) dingTimer_ = 0.0f; + + float progress = 1.0f - (dingTimer_ / DING_DURATION); // 0→1 over duration + float alpha = dingTimer_ < 0.8f ? (dingTimer_ / 0.8f) : 1.0f; // fade out last 0.8s + + ImGuiIO& io = ImGui::GetIO(); + float cx = io.DisplaySize.x * 0.5f; + float cy = io.DisplaySize.y * 0.5f; + float maxR = std::min(cx, cy) * 1.1f; + + ImDrawList* draw = ImGui::GetForegroundDrawList(); + + // 3 expanding golden rings staggered by 0.12s of phase + for (int r = 0; r < 3; r++) { + float phase = progress - r * 0.12f; + if (phase <= 0.0f || phase >= 1.0f) continue; + float ringAlpha = (1.0f - phase) * alpha * 0.9f; + float radius = phase * maxR; + float thickness = 10.0f * (1.0f - phase) + 2.0f; + draw->AddCircle(ImVec2(cx, cy), radius, + IM_COL32(255, 215, 0, (int)(ringAlpha * 255)), + 96, thickness); + } + + // Inner golden glow disk (very transparent) + if (progress < 0.5f) { + float glowAlpha = (1.0f - progress * 2.0f) * alpha * 0.15f; + draw->AddCircleFilled(ImVec2(cx, cy), progress * maxR * 0.6f, + IM_COL32(255, 215, 0, (int)(glowAlpha * 255))); + } + + // "LEVEL X!" text — visible for first 2.2s + if (dingTimer_ > 0.8f) { + ImFont* font = ImGui::GetFont(); + float baseSize = ImGui::GetFontSize(); + float fontSize = baseSize * 2.8f; + + char buf[32]; + snprintf(buf, sizeof(buf), "LEVEL %u!", dingLevel_); + + ImVec2 sz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, buf); + float tx = cx - sz.x * 0.5f; + float ty = cy - sz.y * 0.5f - 20.0f; + + // Drop shadow + draw->AddText(font, fontSize, ImVec2(tx + 3, ty + 3), + IM_COL32(0, 0, 0, (int)(alpha * 200)), buf); + // Gold text + draw->AddText(font, fontSize, ImVec2(tx, ty), + IM_COL32(255, 215, 0, (int)(alpha * 255)), buf); + + // "DING!" subtitle + const char* ding = "DING!"; + float dingSize = baseSize * 1.8f; + ImVec2 dingSz = font->CalcTextSizeA(dingSize, FLT_MAX, 0.0f, ding); + float dx = cx - dingSz.x * 0.5f; + float dy = ty + sz.y + 6.0f; + draw->AddText(font, dingSize, ImVec2(dx + 2, dy + 2), + IM_COL32(0, 0, 0, (int)(alpha * 180)), ding); + draw->AddText(font, dingSize, ImVec2(dx, dy), + IM_COL32(255, 255, 150, (int)(alpha * 255)), ding); + } +} + }} // namespace wowee::ui