diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index e8c22327..1f6a029b 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -573,6 +573,9 @@ public: } uint64_t getAutoAttackTargetGuid() const { return autoAttackTarget; } bool isAggressiveTowardPlayer(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; } + // Timestamp (ms since epoch) of the most recent player melee auto-attack. + // Zero if no swing has occurred this session. + uint64_t getLastMeleeSwingMs() const { return lastMeleeSwingMs_; } const std::vector& getCombatText() const { return combatText; } void updateCombatText(float deltaTime); @@ -2854,6 +2857,7 @@ private: StandStateCallback standStateCallback_; GhostStateCallback ghostStateCallback_; MeleeSwingCallback meleeSwingCallback_; + uint64_t lastMeleeSwingMs_ = 0; // system_clock ms at last player auto-attack swing SpellCastAnimCallback spellCastAnimCallback_; UnitAnimHintCallback unitAnimHintCallback_; UnitMoveFlagsCallback unitMoveFlagsCallback_; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 8ed25e79..f3d3eb2c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -14328,8 +14328,11 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) { bool isPlayerTarget = (data.targetGuid == playerGuid); if (!isPlayerAttacker && !isPlayerTarget) return; // Not our combat - if (isPlayerAttacker && meleeSwingCallback_) { - meleeSwingCallback_(); + if (isPlayerAttacker) { + lastMeleeSwingMs_ = static_cast( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); + if (meleeSwingCallback_) meleeSwingCallback_(); } if (!isPlayerAttacker && npcSwingCallback_) { npcSwingCallback_(data.attackerGuid); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 6a3cc61d..6d86c1a1 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -2977,6 +2977,47 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { ImGui::SetCursorScreenPos(ImVec2(cursor.x, cursor.y + slotH + 2.0f)); } } + + // Melee swing timer — shown when player is auto-attacking + if (gameHandler.isAutoAttacking()) { + const uint64_t lastSwingMs = gameHandler.getLastMeleeSwingMs(); + if (lastSwingMs > 0) { + // Determine weapon speed from the equipped main-hand weapon + uint32_t weaponDelayMs = 2000; // Default: 2.0s unarmed + const auto& mainSlot = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND); + if (!mainSlot.empty() && mainSlot.item.itemId != 0) { + const auto* info = gameHandler.getItemInfo(mainSlot.item.itemId); + if (info && info->delayMs > 0) { + weaponDelayMs = info->delayMs; + } + } + + // Compute elapsed since last swing + uint64_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); + uint64_t elapsedMs = (nowMs >= lastSwingMs) ? (nowMs - lastSwingMs) : 0; + + // Clamp to weapon delay (cap at 1.0 so the bar fills but doesn't exceed) + float pct = std::min(static_cast(elapsedMs) / static_cast(weaponDelayMs), 1.0f); + + // Light silver-orange color indicating auto-attack readiness + ImVec4 swingColor = (pct >= 0.95f) + ? ImVec4(1.0f, 0.75f, 0.15f, 1.0f) // gold when ready to swing + : ImVec4(0.65f, 0.55f, 0.40f, 1.0f); // muted brown-orange while filling + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, swingColor); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.12f, 0.08f, 0.8f)); + char swingLabel[24]; + float remainSec = std::max(0.0f, (weaponDelayMs - static_cast(elapsedMs)) / 1000.0f); + if (pct >= 0.98f) + snprintf(swingLabel, sizeof(swingLabel), "Swing!"); + else + snprintf(swingLabel, sizeof(swingLabel), "%.1fs", remainSec); + ImGui::ProgressBar(pct, ImVec2(-1.0f, 8.0f), swingLabel); + ImGui::PopStyleColor(2); + } + } + ImGui::End(); ImGui::PopStyleColor(2);