From a09bea5e1e123e612a7a0556b42eb2c193ce4c82 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Feb 2026 14:30:54 -0800 Subject: [PATCH] Add spell icons to action bar from SpellIcon.dbc Load spell icons via Spell.dbc field 133 (SpellIconID) -> SpellIcon.dbc (icon path) -> BLP texture from Interface\Icons\. Icons are cached as GL textures and rendered with ImageButton, with cooldown text overlaid on the icon. Falls back to truncated spell name text for missing icons. --- include/ui/game_screen.hpp | 17 +++- src/ui/game_screen.cpp | 170 ++++++++++++++++++++++++++++++------- 2 files changed, 156 insertions(+), 31 deletions(-) diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 84157d54..b426e313 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -7,11 +7,14 @@ #include "ui/inventory_screen.hpp" #include "ui/quest_log_screen.hpp" #include "ui/spellbook_screen.hpp" +#include #include #include #include -namespace wowee { namespace ui { +namespace wowee { +namespace pipeline { class AssetManager; } +namespace ui { /** * In-game screen UI @@ -152,6 +155,16 @@ private: bool actionSpellDbAttempted = false; bool actionSpellDbLoaded = false; std::unordered_map actionSpellNames; + + // Spell icon cache: spellId -> GL texture ID + std::unordered_map spellIconCache_; + // SpellIconID -> icon path (from SpellIcon.dbc) + std::unordered_map spellIconPaths_; + // SpellID -> SpellIconID (from Spell.dbc field 133) + std::unordered_map spellIconIds_; + bool spellIconDbLoaded_ = false; + GLuint getSpellIcon(uint32_t spellId, pipeline::AssetManager* am); }; -}} // namespace wowee::ui +} // namespace ui +} // namespace wowee diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 2c597cab..473a6a1d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -14,6 +14,7 @@ #include "audio/activity_sound_manager.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_loader.hpp" +#include "pipeline/blp_loader.hpp" #include "core/logger.hpp" #include #include @@ -1122,6 +1123,84 @@ void GameScreen::renderWorldMap(game::GameHandler& /* gameHandler */) { // Action Bar (Phase 3) // ============================================================ +GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) { + if (spellId == 0 || !am) return 0; + + // Check cache first + auto cit = spellIconCache_.find(spellId); + if (cit != spellIconCache_.end()) return cit->second; + + // Lazy-load SpellIcon.dbc and Spell.dbc icon IDs + if (!spellIconDbLoaded_) { + spellIconDbLoaded_ = true; + + // Load SpellIcon.dbc: field 0 = ID, field 1 = icon path + auto iconDbc = am->loadDBC("SpellIcon.dbc"); + if (iconDbc && iconDbc->isLoaded()) { + for (uint32_t i = 0; i < iconDbc->getRecordCount(); i++) { + uint32_t id = iconDbc->getUInt32(i, 0); + std::string path = iconDbc->getString(i, 1); + if (!path.empty() && id > 0) { + spellIconPaths_[id] = path; + } + } + } + + // Load Spell.dbc: field 133 = SpellIconID + auto spellDbc = am->loadDBC("Spell.dbc"); + if (spellDbc && spellDbc->isLoaded() && spellDbc->getFieldCount() > 133) { + for (uint32_t i = 0; i < spellDbc->getRecordCount(); i++) { + uint32_t id = spellDbc->getUInt32(i, 0); + uint32_t iconId = spellDbc->getUInt32(i, 133); + if (id > 0 && iconId > 0) { + spellIconIds_[id] = iconId; + } + } + } + } + + // Look up spellId -> SpellIconID -> icon path + auto iit = spellIconIds_.find(spellId); + if (iit == spellIconIds_.end()) { + spellIconCache_[spellId] = 0; + return 0; + } + + auto pit = spellIconPaths_.find(iit->second); + if (pit == spellIconPaths_.end()) { + spellIconCache_[spellId] = 0; + return 0; + } + + // Path from DBC has no extension — append .blp + std::string iconPath = pit->second + ".blp"; + auto blpData = am->readFile(iconPath); + if (blpData.empty()) { + spellIconCache_[spellId] = 0; + return 0; + } + + auto image = pipeline::BLPLoader::load(blpData); + if (!image.isValid()) { + spellIconCache_[spellId] = 0; + return 0; + } + + GLuint texId = 0; + glGenTextures(1, &texId); + glBindTexture(GL_TEXTURE_2D, texId); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0, + GL_RGBA, GL_UNSIGNED_BYTE, image.data.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + + spellIconCache_[spellId] = texId; + return texId; +} + void GameScreen::renderActionBar(game::GameHandler& gameHandler) { auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; @@ -1159,14 +1238,6 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { const auto& slot = bar[i]; bool onCooldown = !slot.isReady(); - 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)); - } - auto getSpellName = [&](uint32_t spellId) -> std::string { if (!actionSpellDbAttempted) { actionSpellDbAttempted = true; @@ -1184,9 +1255,9 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { } uint32_t count = dbc->getRecordCount(); actionSpellNames.reserve(count); - for (uint32_t i = 0; i < count; ++i) { - uint32_t id = dbc->getUInt32(i, 0); - std::string name = dbc->getString(i, nameField); + for (uint32_t r = 0; r < count; ++r) { + uint32_t id = dbc->getUInt32(r, 0); + std::string name = dbc->getString(r, nameField); if (!name.empty() && id > 0) { actionSpellNames[id] = name; } @@ -1200,29 +1271,59 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { return "Spell #" + std::to_string(spellId); }; - char label[32]; - std::string spellName; - if (slot.type == game::ActionBarSlot::SPELL) { - 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) { - snprintf(label, sizeof(label), "Item"); - } else if (slot.type == game::ActionBarSlot::MACRO) { - snprintf(label, sizeof(label), "Macro"); - } else { - snprintf(label, sizeof(label), "--"); + // Try to get icon texture for this slot + GLuint iconTex = 0; + if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) { + iconTex = getSpellIcon(slot.id, assetMgr); } - if (ImGui::Button(label, ImVec2(slotSize, slotSize))) { + 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); + } + clicked = ImGui::ImageButton("##icon", + (ImTextureID)(uintptr_t)iconTex, + ImVec2(slotSize - 4, slotSize - 4), + ImVec2(0, 0), ImVec2(1, 1), + bgColor, tintColor); + } 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) { + 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(); + } + + if (clicked) { if (slot.type == game::ActionBarSlot::SPELL && slot.isReady()) { uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; gameHandler.castSpell(slot.id, target); } } - ImGui::PopStyleColor(); if (ImGui::IsItemHovered() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0) { std::string fullName = getSpellName(slot.id); @@ -1232,8 +1333,19 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { ImGui::EndTooltip(); } - // Cooldown overlay text - if (onCooldown) { + // Cooldown overlay + if (onCooldown && iconTex) { + // Draw cooldown text centered over the icon + ImVec2 btnMin = ImGui::GetItemRectMin(); + ImVec2 btnMax = ImGui::GetItemRectMax(); + char cdText[16]; + snprintf(cdText, sizeof(cdText), "%.0f", slot.cooldownRemaining); + ImVec2 textSize = ImGui::CalcTextSize(cdText); + float cx = btnMin.x + (btnMax.x - btnMin.x - textSize.x) * 0.5f; + float cy = btnMin.y + (btnMax.y - btnMin.y - textSize.y) * 0.5f; + ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), + IM_COL32(255, 255, 0, 255), cdText); + } else if (onCooldown) { char cdText[16]; snprintf(cdText, sizeof(cdText), "%.0f", slot.cooldownRemaining); ImGui::SetCursorPosY(ImGui::GetCursorPosY() - slotSize / 2 - 8);