mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
feat: track and visualize global cooldown (GCD) on action bar
- GameHandler tracks GCD in gcdTotal_/gcdStartedAt_ (time-based) - SMSG_SPELL_COOLDOWN: spellId=0 entries (<=2s) are treated as GCD - castSpell(): optimistically starts 1.5s GCD client-side on cast - Action bar: non-cooldown slots show subtle dark sweep + dim tint during the GCD window, matching WoW standard behavior
This commit is contained in:
parent
61c0b91e39
commit
b6a43d6ce7
3 changed files with 58 additions and 0 deletions
|
|
@ -743,6 +743,17 @@ public:
|
||||||
float getGameTime() const { return gameTime_; }
|
float getGameTime() const { return gameTime_; }
|
||||||
float getTimeSpeed() const { return timeSpeed_; }
|
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::milliseconds>(
|
||||||
|
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)
|
// Weather state (updated by SMSG_WEATHER)
|
||||||
// weatherType: 0=clear, 1=rain, 2=snow, 3=storm/fog
|
// weatherType: 0=clear, 1=rain, 2=snow, 3=storm/fog
|
||||||
uint32_t getWeatherType() const { return weatherType_; }
|
uint32_t getWeatherType() const { return weatherType_; }
|
||||||
|
|
@ -2559,6 +2570,10 @@ private:
|
||||||
float timeSpeed_ = 0.0166f; // Time scale (default: 1 game day = 1 real hour)
|
float timeSpeed_ = 0.0166f; // Time scale (default: 1 game day = 1 real hour)
|
||||||
void handleLoginSetTimeSpeed(network::Packet& packet);
|
void handleLoginSetTimeSpeed(network::Packet& packet);
|
||||||
|
|
||||||
|
// ---- Global Cooldown (GCD) ----
|
||||||
|
float gcdTotal_ = 0.0f;
|
||||||
|
std::chrono::steady_clock::time_point gcdStartedAt_{};
|
||||||
|
|
||||||
// ---- Weather state (SMSG_WEATHER) ----
|
// ---- Weather state (SMSG_WEATHER) ----
|
||||||
uint32_t weatherType_ = 0; // 0=clear, 1=rain, 2=snow, 3=storm
|
uint32_t weatherType_ = 0; // 0=clear, 1=rain, 2=snow, 3=storm
|
||||||
float weatherIntensity_ = 0.0f; // 0.0 to 1.0
|
float weatherIntensity_ = 0.0f; // 0.0 to 1.0
|
||||||
|
|
|
||||||
|
|
@ -14119,6 +14119,10 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
||||||
: CastSpellPacket::build(spellId, target, ++castCount);
|
: CastSpellPacket::build(spellId, target, ++castCount);
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec);
|
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() {
|
void GameHandler::cancelCast() {
|
||||||
|
|
@ -14477,6 +14481,14 @@ void GameHandler::handleSpellCooldown(network::Packet& packet) {
|
||||||
uint32_t cooldownMs = packet.readUInt32();
|
uint32_t cooldownMs = packet.readUInt32();
|
||||||
|
|
||||||
float seconds = cooldownMs / 1000.0f;
|
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;
|
spellCooldowns[spellId] = seconds;
|
||||||
for (auto& slot : actionBar) {
|
for (auto& slot : actionBar) {
|
||||||
bool match = (slot.type == ActionBarSlot::SPELL && slot.id == spellId)
|
bool match = (slot.type == ActionBarSlot::SPELL && slot.id == spellId)
|
||||||
|
|
|
||||||
|
|
@ -4952,6 +4952,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
|
||||||
|
|
||||||
const auto& slot = bar[absSlot];
|
const auto& slot = bar[absSlot];
|
||||||
bool onCooldown = !slot.isReady();
|
bool onCooldown = !slot.isReady();
|
||||||
|
const bool onGCD = gameHandler.isGCDActive() && !onCooldown && !slot.isEmpty();
|
||||||
|
|
||||||
auto getSpellName = [&](uint32_t spellId) -> std::string {
|
auto getSpellName = [&](uint32_t spellId) -> std::string {
|
||||||
std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr);
|
std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr);
|
||||||
|
|
@ -5004,6 +5005,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
|
||||||
ImVec4 tintColor(1, 1, 1, 1);
|
ImVec4 tintColor(1, 1, 1, 1);
|
||||||
ImVec4 bgColor(0.1f, 0.1f, 0.1f, 0.9f);
|
ImVec4 bgColor(0.1f, 0.1f, 0.1f, 0.9f);
|
||||||
if (onCooldown) { tintColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f); }
|
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",
|
clicked = ImGui::ImageButton("##icon",
|
||||||
(ImTextureID)(uintptr_t)iconTex,
|
(ImTextureID)(uintptr_t)iconTex,
|
||||||
ImVec2(slotSize, slotSize),
|
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);
|
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<float>(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
|
// Item stack count overlay — bottom-right corner of icon
|
||||||
if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
|
if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
|
||||||
// Count total of this item across all inventory slots
|
// Count total of this item across all inventory slots
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue