diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 03bcecd4..a182822b 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1035,6 +1035,14 @@ public: const std::string& getDuelChallengerName() const { return duelChallengerName_; } void acceptDuel(); // forfeitDuel() already declared at line ~399 + // Returns remaining duel countdown seconds, or 0 if no active countdown + float getDuelCountdownRemaining() const { + if (duelCountdownMs_ == 0) return 0.0f; + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - duelCountdownStartedAt_).count(); + float rem = (static_cast(duelCountdownMs_) - static_cast(elapsed)) / 1000.0f; + return rem > 0.0f ? rem : 0.0f; + } // ---- Instance lockouts ---- struct InstanceLockout { @@ -2251,6 +2259,8 @@ private: uint64_t duelChallengerGuid_= 0; uint64_t duelFlagGuid_ = 0; std::string duelChallengerName_; + uint32_t duelCountdownMs_ = 0; // 0 = no active countdown + std::chrono::steady_clock::time_point duelCountdownStartedAt_{}; // ---- Guild state ---- std::string guildName_; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index a8ac222d..417a6609 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -293,6 +293,7 @@ private: void renderQuestCompleteToasts(float deltaTime); void renderGroupInvitePopup(game::GameHandler& gameHandler); void renderDuelRequestPopup(game::GameHandler& gameHandler); + void renderDuelCountdown(game::GameHandler& gameHandler); void renderLootRollPopup(game::GameHandler& gameHandler); void renderTradeRequestPopup(game::GameHandler& gameHandler); void renderTradeWindow(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index b68d6d02..ab4b0c99 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3214,9 +3214,16 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_DUEL_INBOUNDS: // Re-entered the duel area; no special action needed. break; - case Opcode::SMSG_DUEL_COUNTDOWN: - // Countdown timer — no action needed; server also sends UNIT_FIELD_FLAGS update. + case Opcode::SMSG_DUEL_COUNTDOWN: { + // uint32 countdown in milliseconds (typically 3000 ms) + if (packet.getSize() - packet.getReadPos() >= 4) { + uint32_t ms = packet.readUInt32(); + duelCountdownMs_ = (ms > 0 && ms <= 30000) ? ms : 3000; + duelCountdownStartedAt_ = std::chrono::steady_clock::now(); + LOG_INFO("SMSG_DUEL_COUNTDOWN: ", duelCountdownMs_, " ms"); + } break; + } case Opcode::SMSG_PARTYKILLLOG: { // uint64 killerGuid + uint64 victimGuid if (packet.getSize() - packet.getReadPos() < 16) break; @@ -10472,6 +10479,7 @@ void GameHandler::handleDuelComplete(network::Packet& packet) { uint8_t started = packet.readUInt8(); // started=1: duel began, started=0: duel was cancelled before starting pendingDuelRequest_ = false; + duelCountdownMs_ = 0; // clear countdown once duel is resolved if (!started) { addSystemChatMessage("The duel was cancelled."); } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index fe76f89c..23159d83 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -482,6 +482,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderBossFrames(gameHandler); renderGroupInvitePopup(gameHandler); renderDuelRequestPopup(gameHandler); + renderDuelCountdown(gameHandler); renderLootRollPopup(gameHandler); renderTradeRequestPopup(gameHandler); renderTradeWindow(gameHandler); @@ -7488,6 +7489,47 @@ void GameScreen::renderDuelRequestPopup(game::GameHandler& gameHandler) { ImGui::End(); } +void GameScreen::renderDuelCountdown(game::GameHandler& gameHandler) { + float remaining = gameHandler.getDuelCountdownRemaining(); + if (remaining <= 0.0f) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + auto* dl = ImGui::GetForegroundDrawList(); + ImFont* font = ImGui::GetFont(); + float fontSize = ImGui::GetFontSize(); + + // Show integer countdown or "Fight!" when under 0.5s + char buf[32]; + if (remaining > 0.5f) { + snprintf(buf, sizeof(buf), "%d", static_cast(std::ceil(remaining))); + } else { + snprintf(buf, sizeof(buf), "Fight!"); + } + + // Large font by scaling — use 4x font size for dramatic effect + float scale = 4.0f; + float scaledSize = fontSize * scale; + ImVec2 textSz = font->CalcTextSizeA(scaledSize, FLT_MAX, 0.0f, buf); + float tx = (screenW - textSz.x) * 0.5f; + float ty = screenH * 0.35f - textSz.y * 0.5f; + + // Pulsing alpha: fades in and out per second + float pulse = 0.75f + 0.25f * std::sin(static_cast(ImGui::GetTime()) * 6.28f); + uint8_t alpha = static_cast(255 * pulse); + + // Color: golden countdown, red "Fight!" + ImU32 color = (remaining > 0.5f) + ? IM_COL32(255, 200, 50, alpha) + : IM_COL32(255, 60, 60, alpha); + + // Drop shadow + dl->AddText(font, scaledSize, ImVec2(tx + 2.0f, ty + 2.0f), IM_COL32(0, 0, 0, alpha / 2), buf); + dl->AddText(font, scaledSize, ImVec2(tx, ty), color, buf); +} + void GameScreen::renderItemTextWindow(game::GameHandler& gameHandler) { if (!gameHandler.isItemTextOpen()) return;