ui,game: add second action bar (Shift+1-12 keybinds, slots 12-23)

Expand action bar from 12 to 24 slots (2 bars × 12). Bar 2 is rendered
above bar 1 and loaded from SMSG_ACTION_BUTTONS slots 12-23. Pressing
Shift+number activates the corresponding bar-2 slot. Drag-and-drop,
cooldown overlays, and tooltips work identically on both bars. Bar 2
fades slightly when all its slots are empty to minimize visual noise.
This commit is contained in:
Kelsi 2026-03-10 06:04:43 -07:00
parent 753790ae47
commit 90b8cccac5
3 changed files with 235 additions and 244 deletions

View file

@ -588,8 +588,10 @@ public:
const std::unordered_map<uint32_t, TalentTabEntry>& getAllTalentTabs() const { return talentTabCache_; }
void loadTalentDbc();
// Action bar
static constexpr int ACTION_BAR_SLOTS = 12;
// Action bar — 2 bars × 12 slots = 24 total
static constexpr int SLOTS_PER_BAR = 12;
static constexpr int ACTION_BARS = 2;
static constexpr int ACTION_BAR_SLOTS = SLOTS_PER_BAR * ACTION_BARS; // 24
std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() { return actionBar; }
const std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() const { return actionBar; }
void setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id);
@ -1839,7 +1841,7 @@ private:
bool areaTriggerSuppressFirst_ = false; // suppress first check after map transfer
float castTimeTotal = 0.0f;
std::array<ActionBarSlot, 12> actionBar{};
std::array<ActionBarSlot, ACTION_BAR_SLOTS> actionBar{};
std::vector<AuraSlot> playerAuras;
std::vector<AuraSlot> targetAuras;
uint64_t petGuid_ = 0;

View file

@ -3333,12 +3333,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
/*uint8_t mode =*/ packet.readUInt8();
rem--;
constexpr int SERVER_BAR_SLOTS = 144;
constexpr int OUR_BAR_SLOTS = 12; // our actionBar array size
for (int i = 0; i < SERVER_BAR_SLOTS; ++i) {
if (rem < 4) break;
uint32_t packed = packet.readUInt32();
rem -= 4;
if (i >= OUR_BAR_SLOTS) continue; // only load first bar
if (i >= ACTION_BAR_SLOTS) continue; // only load bars 1 and 2
if (packed == 0) {
// Empty slot — only clear if not already set to Attack/Hearthstone defaults
// so we don't wipe hardcoded fallbacks when the server sends zeros.

View file

@ -1417,14 +1417,16 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
SDL_SCANCODE_5, SDL_SCANCODE_6, SDL_SCANCODE_7, SDL_SCANCODE_8,
SDL_SCANCODE_9, SDL_SCANCODE_0, SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS
};
for (int i = 0; i < 12; ++i) {
const bool shiftDown = input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT);
const auto& bar = gameHandler.getActionBar();
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
if (input.isKeyJustPressed(actionBarKeys[i])) {
const auto& bar = gameHandler.getActionBar();
if (bar[i].type == game::ActionBarSlot::SPELL && bar[i].isReady()) {
int slotIdx = shiftDown ? (game::GameHandler::SLOTS_PER_BAR + i) : i;
if (bar[slotIdx].type == game::ActionBarSlot::SPELL && bar[slotIdx].isReady()) {
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
gameHandler.castSpell(bar[i].id, target);
} else if (bar[i].type == game::ActionBarSlot::ITEM && bar[i].id != 0) {
gameHandler.useItemById(bar[i].id);
gameHandler.castSpell(bar[slotIdx].id, target);
} else if (bar[slotIdx].type == game::ActionBarSlot::ITEM && bar[slotIdx].id != 0) {
gameHandler.useItemById(bar[slotIdx].id);
}
}
}
@ -3842,262 +3844,250 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f));
if (ImGui::Begin("##ActionBar", nullptr, flags)) {
const auto& bar = gameHandler.getActionBar();
static const char* keyLabels[] = {"1","2","3","4","5","6","7","8","9","0","-","="};
// Per-slot rendering lambda — shared by both action bars
const auto& bar = gameHandler.getActionBar();
static const char* keyLabels1[] = {"1","2","3","4","5","6","7","8","9","0","-","="};
// "⇧N" labels for bar 2 (UTF-8: E2 87 A7 = U+21E7 UPWARDS WHITE ARROW)
static const char* keyLabels2[] = {
"\xe2\x87\xa7" "1", "\xe2\x87\xa7" "2", "\xe2\x87\xa7" "3",
"\xe2\x87\xa7" "4", "\xe2\x87\xa7" "5", "\xe2\x87\xa7" "6",
"\xe2\x87\xa7" "7", "\xe2\x87\xa7" "8", "\xe2\x87\xa7" "9",
"\xe2\x87\xa7" "0", "\xe2\x87\xa7" "-", "\xe2\x87\xa7" "="
};
for (int i = 0; i < 12; ++i) {
if (i > 0) ImGui::SameLine(0, spacing);
auto renderBarSlot = [&](int absSlot, const char* keyLabel) {
ImGui::BeginGroup();
ImGui::PushID(absSlot);
ImGui::BeginGroup();
ImGui::PushID(i);
const auto& slot = bar[absSlot];
bool onCooldown = !slot.isReady();
const auto& slot = bar[i];
bool onCooldown = !slot.isReady();
auto getSpellName = [&](uint32_t spellId) -> std::string {
std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr);
if (!name.empty()) return name;
return "Spell #" + std::to_string(spellId);
};
auto getSpellName = [&](uint32_t spellId) -> std::string {
std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr);
if (!name.empty()) return name;
return "Spell #" + std::to_string(spellId);
};
// Try to get icon texture for this slot
VkDescriptorSet iconTex = VK_NULL_HANDLE;
const game::ItemDef* barItemDef = nullptr;
uint32_t itemDisplayInfoId = 0;
std::string itemNameFromQuery;
if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) {
iconTex = getSpellIcon(slot.id, assetMgr);
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
// Search backpack
auto& inv = gameHandler.getInventory();
for (int bi = 0; bi < inv.getBackpackSize(); bi++) {
const auto& bs = inv.getBackpackSlot(bi);
if (!bs.empty() && bs.item.itemId == slot.id) {
barItemDef = &bs.item;
break;
}
}
// Search equipped slots
if (!barItemDef) {
for (int ei = 0; ei < game::Inventory::NUM_EQUIP_SLOTS; ei++) {
const auto& es = inv.getEquipSlot(static_cast<game::EquipSlot>(ei));
if (!es.empty() && es.item.itemId == slot.id) {
barItemDef = &es.item;
break;
}
}
}
// Search extra bags
if (!barItemDef) {
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS && !barItemDef; bag++) {
for (int si = 0; si < inv.getBagSize(bag); si++) {
const auto& bs = inv.getBagSlot(bag, si);
if (!bs.empty() && bs.item.itemId == slot.id) {
barItemDef = &bs.item;
break;
}
}
}
}
if (barItemDef && barItemDef->displayInfoId != 0) {
itemDisplayInfoId = barItemDef->displayInfoId;
}
// Fallback: use item info cache (from server query responses)
if (itemDisplayInfoId == 0) {
if (auto* info = gameHandler.getItemInfo(slot.id)) {
itemDisplayInfoId = info->displayInfoId;
if (itemNameFromQuery.empty() && !info->name.empty())
itemNameFromQuery = info->name;
}
}
if (itemDisplayInfoId != 0) {
iconTex = inventoryScreen.getItemIcon(itemDisplayInfoId);
// Try to get icon texture for this slot
VkDescriptorSet iconTex = VK_NULL_HANDLE;
const game::ItemDef* barItemDef = nullptr;
uint32_t itemDisplayInfoId = 0;
std::string itemNameFromQuery;
if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) {
iconTex = getSpellIcon(slot.id, assetMgr);
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
auto& inv = gameHandler.getInventory();
for (int bi = 0; bi < inv.getBackpackSize(); bi++) {
const auto& bs = inv.getBackpackSlot(bi);
if (!bs.empty() && bs.item.itemId == slot.id) { barItemDef = &bs.item; break; }
}
if (!barItemDef) {
for (int ei = 0; ei < game::Inventory::NUM_EQUIP_SLOTS; ei++) {
const auto& es = inv.getEquipSlot(static_cast<game::EquipSlot>(ei));
if (!es.empty() && es.item.itemId == slot.id) { barItemDef = &es.item; break; }
}
}
bool clicked = false;
if (iconTex) {
// Render icon-based button
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);
bgColor = ImVec4(0.1f, 0.1f, 0.1f, 0.8f);
if (!barItemDef) {
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS && !barItemDef; bag++) {
for (int si = 0; si < inv.getBagSize(bag); si++) {
const auto& bs = inv.getBagSlot(bag, si);
if (!bs.empty() && bs.item.itemId == slot.id) { barItemDef = &bs.item; break; }
}
}
clicked = ImGui::ImageButton("##icon",
(ImTextureID)(uintptr_t)iconTex,
ImVec2(slotSize, slotSize),
ImVec2(0, 0), ImVec2(1, 1),
bgColor, tintColor);
}
if (barItemDef && barItemDef->displayInfoId != 0)
itemDisplayInfoId = barItemDef->displayInfoId;
if (itemDisplayInfoId == 0) {
if (auto* info = gameHandler.getItemInfo(slot.id)) {
itemDisplayInfoId = info->displayInfoId;
if (itemNameFromQuery.empty() && !info->name.empty())
itemNameFromQuery = info->name;
}
}
if (itemDisplayInfoId != 0)
iconTex = inventoryScreen.getItemIcon(itemDisplayInfoId);
}
bool clicked = false;
if (iconTex) {
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); }
clicked = ImGui::ImageButton("##icon",
(ImTextureID)(uintptr_t)iconTex,
ImVec2(slotSize, slotSize),
ImVec2(0, 0), ImVec2(1, 1),
bgColor, tintColor);
} else {
if (onCooldown) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f));
else if (slot.isEmpty())ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f));
else ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f));
char label[32];
if (slot.type == game::ActionBarSlot::SPELL) {
std::string spellName = getSpellName(slot.id);
if (spellName.size() > 6) spellName = spellName.substr(0, 6);
snprintf(label, sizeof(label), "%s", spellName.c_str());
} else if (slot.type == game::ActionBarSlot::ITEM && barItemDef) {
std::string itemName = barItemDef->name;
if (itemName.size() > 6) itemName = itemName.substr(0, 6);
snprintf(label, sizeof(label), "%s", itemName.c_str());
} else if (slot.type == game::ActionBarSlot::ITEM) {
snprintf(label, sizeof(label), "Item");
} else if (slot.type == game::ActionBarSlot::MACRO) {
snprintf(label, sizeof(label), "Macro");
} else {
// Fallback to text button
if (onCooldown) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f));
} else if (slot.isEmpty()) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f));
} else {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f));
}
char label[32];
if (slot.type == game::ActionBarSlot::SPELL) {
std::string spellName = getSpellName(slot.id);
if (spellName.size() > 6) spellName = spellName.substr(0, 6);
snprintf(label, sizeof(label), "%s", spellName.c_str());
} else if (slot.type == game::ActionBarSlot::ITEM && barItemDef) {
std::string itemName = barItemDef->name;
if (itemName.size() > 6) itemName = itemName.substr(0, 6);
snprintf(label, sizeof(label), "%s", itemName.c_str());
} else if (slot.type == game::ActionBarSlot::ITEM) {
snprintf(label, sizeof(label), "Item");
} else if (slot.type == game::ActionBarSlot::MACRO) {
snprintf(label, sizeof(label), "Macro");
} else {
snprintf(label, sizeof(label), "--");
}
clicked = ImGui::Button(label, ImVec2(slotSize, slotSize));
ImGui::PopStyleColor();
snprintf(label, sizeof(label), "--");
}
clicked = ImGui::Button(label, ImVec2(slotSize, slotSize));
ImGui::PopStyleColor();
}
bool rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right);
bool hoveredOnRelease = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
ImGui::IsMouseReleased(ImGuiMouseButton_Left);
bool rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right);
bool hoveredOnRelease = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
ImGui::IsMouseReleased(ImGuiMouseButton_Left);
// Drop dragged spell from spellbook onto this slot
// (mouse release over slot — button click won't fire since press was in spellbook)
if (hoveredOnRelease && spellbookScreen.isDraggingSpell()) {
gameHandler.setActionBarSlot(i, game::ActionBarSlot::SPELL,
spellbookScreen.getDragSpellId());
spellbookScreen.consumeDragSpell();
} else if (hoveredOnRelease && inventoryScreen.isHoldingItem()) {
// Drop held item from inventory onto action bar
const auto& held = inventoryScreen.getHeldItem();
gameHandler.setActionBarSlot(i, game::ActionBarSlot::ITEM, held.itemId);
inventoryScreen.returnHeldItem(gameHandler.getInventory());
} else if (clicked && actionBarDragSlot_ >= 0) {
// Dropping a dragged action bar slot onto another slot - swap or place
if (i != actionBarDragSlot_) {
const auto& dragSrc = bar[actionBarDragSlot_];
auto srcType = dragSrc.type;
auto srcId = dragSrc.id;
gameHandler.setActionBarSlot(actionBarDragSlot_, slot.type, slot.id);
gameHandler.setActionBarSlot(i, srcType, srcId);
}
actionBarDragSlot_ = -1;
actionBarDragIcon_ = 0;
} else if (clicked && !slot.isEmpty()) {
// Left-click on non-empty slot: cast spell or use item
if (slot.type == game::ActionBarSlot::SPELL && slot.isReady()) {
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
gameHandler.castSpell(slot.id, target);
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
gameHandler.useItemById(slot.id);
}
} else if (rightClicked && !slot.isEmpty()) {
// Right-click on non-empty slot: pick up for dragging
actionBarDragSlot_ = i;
actionBarDragIcon_ = iconTex;
if (hoveredOnRelease && spellbookScreen.isDraggingSpell()) {
gameHandler.setActionBarSlot(absSlot, game::ActionBarSlot::SPELL,
spellbookScreen.getDragSpellId());
spellbookScreen.consumeDragSpell();
} else if (hoveredOnRelease && inventoryScreen.isHoldingItem()) {
const auto& held = inventoryScreen.getHeldItem();
gameHandler.setActionBarSlot(absSlot, game::ActionBarSlot::ITEM, held.itemId);
inventoryScreen.returnHeldItem(gameHandler.getInventory());
} else if (clicked && actionBarDragSlot_ >= 0) {
if (absSlot != actionBarDragSlot_) {
const auto& dragSrc = bar[actionBarDragSlot_];
gameHandler.setActionBarSlot(actionBarDragSlot_, slot.type, slot.id);
gameHandler.setActionBarSlot(absSlot, dragSrc.type, dragSrc.id);
}
actionBarDragSlot_ = -1;
actionBarDragIcon_ = 0;
} else if (clicked && !slot.isEmpty()) {
if (slot.type == game::ActionBarSlot::SPELL && slot.isReady()) {
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
gameHandler.castSpell(slot.id, target);
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
gameHandler.useItemById(slot.id);
}
} else if (rightClicked && !slot.isEmpty()) {
actionBarDragSlot_ = absSlot;
actionBarDragIcon_ = iconTex;
}
// Tooltip
if (ImGui::IsItemHovered() && !slot.isEmpty() && slot.id != 0) {
ImGui::BeginTooltip();
if (slot.type == game::ActionBarSlot::SPELL) {
std::string fullName = getSpellName(slot.id);
ImGui::Text("%s", fullName.c_str());
// Hearthstone: show bind point info
if (slot.id == 8690) {
uint32_t mapId = 0;
glm::vec3 pos;
if (gameHandler.getHomeBind(mapId, pos)) {
const char* mapName = "Unknown";
switch (mapId) {
case 0: mapName = "Eastern Kingdoms"; break;
case 1: mapName = "Kalimdor"; break;
case 530: mapName = "Outland"; break;
case 571: mapName = "Northrend"; break;
}
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f),
"Home: %s", mapName);
// Tooltip
if (ImGui::IsItemHovered() && !slot.isEmpty() && slot.id != 0) {
ImGui::BeginTooltip();
if (slot.type == game::ActionBarSlot::SPELL) {
ImGui::Text("%s", getSpellName(slot.id).c_str());
if (slot.id == 8690) {
uint32_t mapId = 0; glm::vec3 pos;
if (gameHandler.getHomeBind(mapId, pos)) {
const char* mapName = "Unknown";
switch (mapId) {
case 0: mapName = "Eastern Kingdoms"; break;
case 1: mapName = "Kalimdor"; break;
case 530: mapName = "Outland"; break;
case 571: mapName = "Northrend"; break;
}
ImGui::TextDisabled("Use: Teleport home");
}
} else if (slot.type == game::ActionBarSlot::ITEM) {
if (barItemDef && !barItemDef->name.empty()) {
ImGui::Text("%s", barItemDef->name.c_str());
} else if (!itemNameFromQuery.empty()) {
ImGui::Text("%s", itemNameFromQuery.c_str());
} else {
ImGui::Text("Item #%u", slot.id);
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", mapName);
}
ImGui::TextDisabled("Use: Teleport home");
}
// Show cooldown time remaining
if (onCooldown) {
float cd = slot.cooldownRemaining;
if (cd >= 60.0f) {
int mins = static_cast<int>(cd) / 60;
int secs = static_cast<int>(cd) % 60;
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
"Cooldown: %d min %d sec", mins, secs);
} else {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
"Cooldown: %.1f sec", cd);
}
}
ImGui::EndTooltip();
} else if (slot.type == game::ActionBarSlot::ITEM) {
if (barItemDef && !barItemDef->name.empty())
ImGui::Text("%s", barItemDef->name.c_str());
else if (!itemNameFromQuery.empty())
ImGui::Text("%s", itemNameFromQuery.c_str());
else
ImGui::Text("Item #%u", slot.id);
}
// Cooldown overlay: WoW-style clock-sweep + time text
if (onCooldown) {
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();
// Dark sweep over the elapsed fraction, starting at 12 o'clock
float total = (slot.cooldownTotal > 0.0f) ? slot.cooldownTotal : 1.0f;
float elapsed = total - slot.cooldownRemaining;
float elapsedFrac = std::min(1.0f, std::max(0.0f, elapsed / total));
if (elapsedFrac > 0.005f) {
constexpr int N_SEGS = 32;
float startAngle = -IM_PI * 0.5f;
float endAngle = startAngle + elapsedFrac * 2.0f * IM_PI;
float fanR = r * 1.5f; // reach the icon corners
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, 170));
}
// Remaining-time text: white with drop-shadow
char cdText[16];
float cd = slot.cooldownRemaining;
if (cd >= 60.0f) {
snprintf(cdText, sizeof(cdText), "%dm", static_cast<int>(cd) / 60);
} else {
snprintf(cdText, sizeof(cdText), "%.0f", cd);
if (cd >= 60.0f)
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
"Cooldown: %d min %d sec", (int)cd/60, (int)cd%60);
else
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Cooldown: %.1f sec", cd);
}
ImGui::EndTooltip();
}
// Cooldown overlay: WoW-style clock-sweep + time text
if (onCooldown) {
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 total = (slot.cooldownTotal > 0.0f) ? slot.cooldownTotal : 1.0f;
float elapsed = total - slot.cooldownRemaining;
float elapsedFrac = std::min(1.0f, std::max(0.0f, elapsed / total));
if (elapsedFrac > 0.005f) {
constexpr int N_SEGS = 32;
float startAngle = -IM_PI * 0.5f;
float endAngle = startAngle + elapsedFrac * 2.0f * IM_PI;
float fanR = r * 1.5f;
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);
}
ImVec2 textSize = ImGui::CalcTextSize(cdText);
float tx = cx - textSize.x * 0.5f;
float ty = cy - textSize.y * 0.5f;
dl->AddText(ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 220), cdText);
dl->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 255), cdText);
dl->AddConvexPolyFilled(pts, N_SEGS + 2, IM_COL32(0, 0, 0, 170));
}
// Key label below
ImGui::TextDisabled("%s", keyLabels[i]);
char cdText[16];
float cd = slot.cooldownRemaining;
if (cd >= 60.0f) snprintf(cdText, sizeof(cdText), "%dm", (int)cd / 60);
else snprintf(cdText, sizeof(cdText), "%.0f", cd);
ImVec2 textSize = ImGui::CalcTextSize(cdText);
float tx = cx - textSize.x * 0.5f;
float ty = cy - textSize.y * 0.5f;
dl->AddText(ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 220), cdText);
dl->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 255), cdText);
}
ImGui::PopID();
ImGui::EndGroup();
// Key label below
ImGui::TextDisabled("%s", keyLabel);
ImGui::PopID();
ImGui::EndGroup();
};
// Bar 2 (slots 12-23) — only show if at least one slot is populated
{
bool bar2HasContent = false;
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i)
if (!bar[game::GameHandler::SLOTS_PER_BAR + i].isEmpty()) { bar2HasContent = true; break; }
float bar2Y = barY - barH - 2.0f;
ImGui::SetNextWindowPos(ImVec2(barX, bar2Y), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg,
bar2HasContent ? ImVec4(0.05f, 0.05f, 0.05f, 0.85f) : ImVec4(0.05f, 0.05f, 0.05f, 0.4f));
if (ImGui::Begin("##ActionBar2", nullptr, flags)) {
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
if (i > 0) ImGui::SameLine(0, spacing);
renderBarSlot(game::GameHandler::SLOTS_PER_BAR + i, keyLabels2[i]);
}
}
ImGui::End();
ImGui::PopStyleColor();
ImGui::PopStyleVar(4);
}
// Bar 1 (slots 0-11)
if (ImGui::Begin("##ActionBar", nullptr, flags)) {
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
if (i > 0) ImGui::SameLine(0, spacing);
renderBarSlot(i, keyLabels1[i]);
}
}
ImGui::End();