diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 52ecb967..8dbebe1d 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -743,6 +743,17 @@ public: float getGameTime() const { return gameTime_; } float getTimeSpeed() const { return timeSpeed_; } + // Global Cooldown (GCD) — set when the server sends a spellId=0 cooldown entry + float getGCDRemaining() const { + if (gcdTotal_ <= 0.0f) return 0.0f; + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - gcdStartedAt_).count() / 1000.0f; + float rem = gcdTotal_ - elapsed; + return rem > 0.0f ? rem : 0.0f; + } + float getGCDTotal() const { return gcdTotal_; } + bool isGCDActive() const { return getGCDRemaining() > 0.0f; } + // Weather state (updated by SMSG_WEATHER) // weatherType: 0=clear, 1=rain, 2=snow, 3=storm/fog uint32_t getWeatherType() const { return weatherType_; } @@ -2559,6 +2570,10 @@ private: float timeSpeed_ = 0.0166f; // Time scale (default: 1 game day = 1 real hour) void handleLoginSetTimeSpeed(network::Packet& packet); + // ---- Global Cooldown (GCD) ---- + float gcdTotal_ = 0.0f; + std::chrono::steady_clock::time_point gcdStartedAt_{}; + // ---- Weather state (SMSG_WEATHER) ---- uint32_t weatherType_ = 0; // 0=clear, 1=rain, 2=snow, 3=storm float weatherIntensity_ = 0.0f; // 0.0 to 1.0 diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 7168a37d..3f1d9b8d 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -14119,6 +14119,10 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { : CastSpellPacket::build(spellId, target, ++castCount); socket->send(packet); LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec); + + // Optimistically start GCD immediately on cast — server will confirm or override + gcdTotal_ = 1.5f; + gcdStartedAt_ = std::chrono::steady_clock::now(); } void GameHandler::cancelCast() { @@ -14477,6 +14481,14 @@ void GameHandler::handleSpellCooldown(network::Packet& packet) { uint32_t cooldownMs = packet.readUInt32(); float seconds = cooldownMs / 1000.0f; + + // spellId=0 is the Global Cooldown marker (server sends it for GCD triggers) + if (spellId == 0 && cooldownMs > 0 && cooldownMs <= 2000) { + gcdTotal_ = seconds; + gcdStartedAt_ = std::chrono::steady_clock::now(); + continue; + } + spellCooldowns[spellId] = seconds; for (auto& slot : actionBar) { bool match = (slot.type == ActionBarSlot::SPELL && slot.id == spellId) diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index fcfbcd8a..40cb4207 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -4952,6 +4952,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { const auto& slot = bar[absSlot]; bool onCooldown = !slot.isReady(); + const bool onGCD = gameHandler.isGCDActive() && !onCooldown && !slot.isEmpty(); auto getSpellName = [&](uint32_t spellId) -> std::string { std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr); @@ -5004,6 +5005,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { ImVec4 tintColor(1, 1, 1, 1); ImVec4 bgColor(0.1f, 0.1f, 0.1f, 0.9f); if (onCooldown) { tintColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f); } + else if (onGCD) { tintColor = ImVec4(0.6f, 0.6f, 0.6f, 0.85f); } clicked = ImGui::ImageButton("##icon", (ImTextureID)(uintptr_t)iconTex, ImVec2(slotSize, slotSize), @@ -5188,6 +5190,35 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { dl->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 255), cdText); } + // GCD overlay — subtle dark fan sweep (thinner/lighter than regular cooldown) + if (onGCD) { + ImVec2 btnMin = ImGui::GetItemRectMin(); + ImVec2 btnMax = ImGui::GetItemRectMax(); + float cx = (btnMin.x + btnMax.x) * 0.5f; + float cy = (btnMin.y + btnMax.y) * 0.5f; + float r = (btnMax.x - btnMin.x) * 0.5f; + auto* dl = ImGui::GetWindowDrawList(); + float gcdRem = gameHandler.getGCDRemaining(); + float gcdTotal = gameHandler.getGCDTotal(); + if (gcdTotal > 0.0f) { + float elapsed = gcdTotal - gcdRem; + float elapsedFrac = std::min(1.0f, std::max(0.0f, elapsed / gcdTotal)); + if (elapsedFrac > 0.005f) { + constexpr int N_SEGS = 24; + float startAngle = -IM_PI * 0.5f; + float endAngle = startAngle + elapsedFrac * 2.0f * IM_PI; + float fanR = r * 1.4f; + ImVec2 pts[N_SEGS + 2]; + pts[0] = ImVec2(cx, cy); + for (int s = 0; s <= N_SEGS; ++s) { + float a = startAngle + (endAngle - startAngle) * s / static_cast(N_SEGS); + pts[s + 1] = ImVec2(cx + std::cos(a) * fanR, cy + std::sin(a) * fanR); + } + dl->AddConvexPolyFilled(pts, N_SEGS + 2, IM_COL32(0, 0, 0, 110)); + } + } + } + // Item stack count overlay — bottom-right corner of icon if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) { // Count total of this item across all inventory slots