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).
This commit is contained in:
Kelsi 2026-03-17 20:46:41 -07:00
parent d44d462686
commit 48cb7df4b4
2 changed files with 118 additions and 0 deletions

View file

@ -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);

View file

@ -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<uint32_t, std::vector<SkillEntry>> 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<int>(e.skillId));
// Name column
ImGui::TextUnformatted(displayName);
ImGui::SameLine(170.0f);
// Progress bar
float fraction = (maxVal > 0) ? static_cast<float>(val) / static_cast<float>(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