From 48cb7df4b4d6208edce7e126f8f0384f067f92c9 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 17 Mar 2026 20:46:41 -0700 Subject: [PATCH] feat: add Skills/Professions window (K key) with per-category progress bars Implements renderSkillsWindow() showing all player skills grouped by DBC category (Professions, Secondary Skills, Class Skills, Weapon Skills, Armor, Languages) with value/max progress bars and a bonus breakdown tooltip. Hooked up to the TOGGLE_SKILLS keybinding (K by default). --- include/ui/game_screen.hpp | 4 ++ src/ui/game_screen.cpp | 114 +++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 98b93b67..52bcca78 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -438,6 +438,10 @@ private: char achievementSearchBuf_[128] = {}; void renderAchievementWindow(game::GameHandler& gameHandler); + // Skills / Professions window (K key) + bool showSkillsWindow_ = false; + void renderSkillsWindow(game::GameHandler& gameHandler); + // Titles window bool showTitlesWindow_ = false; void renderTitlesWindow(game::GameHandler& gameHandler); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index e0ed2b03..b7cc3135 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -718,6 +718,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderWhoWindow(gameHandler); renderCombatLog(gameHandler); renderAchievementWindow(gameHandler); + renderSkillsWindow(gameHandler); renderTitlesWindow(gameHandler); renderEquipSetWindow(gameHandler); renderGmTicketWindow(gameHandler); @@ -2766,6 +2767,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) { showAchievementWindow_ = !showAchievementWindow_; } + if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SKILLS)) { + showSkillsWindow_ = !showSkillsWindow_; + } // Toggle Titles window with H (hero/title screen — no conflicting keybinding) if (input.isKeyJustPressed(SDL_SCANCODE_H) && !ImGui::GetIO().WantCaptureKeyboard) { @@ -22702,4 +22706,114 @@ void GameScreen::renderEquipSetWindow(game::GameHandler& gameHandler) { ImGui::End(); } +void GameScreen::renderSkillsWindow(game::GameHandler& gameHandler) { + if (!showSkillsWindow_) return; + + ImGui::SetNextWindowSize(ImVec2(380, 480), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(220, 130), ImGuiCond_FirstUseEver); + + if (!ImGui::Begin("Skills & Professions", &showSkillsWindow_)) { + ImGui::End(); + return; + } + + const auto& skills = gameHandler.getPlayerSkills(); + if (skills.empty()) { + ImGui::TextDisabled("No skill data received yet."); + ImGui::End(); + return; + } + + // Organise skills by category + // WoW SkillLine.dbc categories: 6=Weapon, 7=Class, 8=Armor, 9=Secondary, 11=Professions, others=Misc + struct SkillEntry { + uint32_t skillId; + const game::PlayerSkill* skill; + }; + std::map> byCategory; + for (const auto& [id, sk] : skills) { + uint32_t cat = gameHandler.getSkillCategory(id); + byCategory[cat].push_back({id, &sk}); + } + + static const struct { uint32_t cat; const char* label; } kCatOrder[] = { + {11, "Professions"}, + { 9, "Secondary Skills"}, + { 7, "Class Skills"}, + { 6, "Weapon Skills"}, + { 8, "Armor"}, + { 5, "Languages"}, + { 0, "Other"}, + }; + + // Collect handled categories to fall back to "Other" for unknowns + static const uint32_t kKnownCats[] = {11, 9, 7, 6, 8, 5}; + + // Redirect unknown categories into bucket 0 + for (auto& [cat, vec] : byCategory) { + bool known = false; + for (uint32_t kc : kKnownCats) if (cat == kc) { known = true; break; } + if (!known && cat != 0) { + auto& other = byCategory[0]; + other.insert(other.end(), vec.begin(), vec.end()); + vec.clear(); + } + } + + ImGui::BeginChild("##skillscroll", ImVec2(0, 0), false); + + for (const auto& [cat, label] : kCatOrder) { + auto it = byCategory.find(cat); + if (it == byCategory.end() || it->second.empty()) continue; + + auto& entries = it->second; + // Sort alphabetically within each category + std::sort(entries.begin(), entries.end(), [&](const SkillEntry& a, const SkillEntry& b) { + return gameHandler.getSkillName(a.skillId) < gameHandler.getSkillName(b.skillId); + }); + + if (ImGui::CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen)) { + for (const auto& e : entries) { + const std::string& name = gameHandler.getSkillName(e.skillId); + const char* displayName = name.empty() ? "Unknown" : name.c_str(); + uint16_t val = e.skill->effectiveValue(); + uint16_t maxVal = e.skill->maxValue; + + ImGui::PushID(static_cast(e.skillId)); + + // Name column + ImGui::TextUnformatted(displayName); + ImGui::SameLine(170.0f); + + // Progress bar + float fraction = (maxVal > 0) ? static_cast(val) / static_cast(maxVal) : 0.0f; + char overlay[32]; + snprintf(overlay, sizeof(overlay), "%u / %u", val, maxVal); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.20f, 0.55f, 0.20f, 1.0f)); + ImGui::ProgressBar(fraction, ImVec2(160.0f, 14.0f), overlay); + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("%s", displayName); + ImGui::Separator(); + ImGui::Text("Base: %u", e.skill->value); + if (e.skill->bonusPerm > 0) + ImGui::Text("Permanent bonus: +%u", e.skill->bonusPerm); + if (e.skill->bonusTemp > 0) + ImGui::Text("Temporary bonus: +%u", e.skill->bonusTemp); + ImGui::Text("Max: %u", maxVal); + ImGui::EndTooltip(); + } + + ImGui::PopID(); + } + ImGui::Spacing(); + } + } + + ImGui::EndChild(); + ImGui::End(); +} + }} // namespace wowee::ui