feat: add melee swing timer bar to player frame during auto-attack
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run

Shows a thin progress bar below the player health/power bars whenever
the player is auto-attacking. The bar fills from the last swing timestamp
to the next expected swing based on the main-hand weapon's delay (from
ItemQueryResponseData::delayMs). Falls back to 2.0s for unarmed. Turns
gold and shows "Swing!" when the timer is complete to signal readiness.
Hides when not auto-attacking.
This commit is contained in:
Kelsi 2026-03-12 20:05:36 -07:00
parent a7261a0d15
commit a87d62abf8
3 changed files with 50 additions and 2 deletions

View file

@ -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<CombatTextEntry>& 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_;

View file

@ -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<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count());
if (meleeSwingCallback_) meleeSwingCallback_();
}
if (!isPlayerAttacker && npcSwingCallback_) {
npcSwingCallback_(data.attackerGuid);

View file

@ -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<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
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<float>(elapsedMs) / static_cast<float>(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<float>(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);