From c9cfa864bf35e5ab97dd74ce23b272f6de0bc996 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 11 Mar 2026 22:07:44 -0700 Subject: [PATCH] Add Achievements tab to character screen with search filter --- include/game/game_handler.hpp | 7 +++++ src/ui/inventory_screen.cpp | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index adbd0e33..5f6ad349 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1215,6 +1215,13 @@ public: using AchievementEarnedCallback = std::function; void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); } const std::unordered_set& getEarnedAchievements() const { return earnedAchievements_; } + /// Returns the name of an achievement by ID, or empty string if unknown. + const std::string& getAchievementName(uint32_t id) const { + auto it = achievementNameCache_.find(id); + if (it != achievementNameCache_.end()) return it->second; + static const std::string kEmpty; + return kEmpty; + } // Server-triggered music callback — fires when SMSG_PLAY_MUSIC is received. // The soundId corresponds to a SoundEntries.dbc record. The receiver is diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index b1328524..c6e04f38 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -1180,6 +1180,57 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("Achievements")) { + const auto& earned = gameHandler.getEarnedAchievements(); + if (earned.empty()) { + ImGui::Spacing(); + ImGui::TextDisabled("No achievements earned yet."); + } else { + static char achieveFilter[128] = {}; + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##achsearch", "Search achievements...", + achieveFilter, sizeof(achieveFilter)); + ImGui::Separator(); + + char filterLower[128]; + for (size_t i = 0; i < sizeof(achieveFilter); ++i) + filterLower[i] = static_cast(tolower(static_cast(achieveFilter[i]))); + + ImGui::BeginChild("##AchList", ImVec2(0, 0), false); + // Sort by ID for stable ordering + std::vector sortedIds(earned.begin(), earned.end()); + std::sort(sortedIds.begin(), sortedIds.end()); + int shown = 0; + for (uint32_t id : sortedIds) { + const std::string& name = gameHandler.getAchievementName(id); + const char* displayName = name.empty() ? nullptr : name.c_str(); + if (displayName == nullptr) continue; // skip unknown achievements + + // Apply filter + if (filterLower[0] != '\0') { + // simple case-insensitive substring match + std::string lower; + lower.reserve(name.size()); + for (char c : name) lower += static_cast(tolower(static_cast(c))); + if (lower.find(filterLower) == std::string::npos) continue; + } + + ImGui::PushID(static_cast(id)); + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "[Achievement]"); + ImGui::SameLine(); + ImGui::Text("%s", displayName); + ImGui::PopID(); + ++shown; + } + if (shown == 0 && filterLower[0] != '\0') { + ImGui::TextDisabled("No achievements match the filter."); + } + ImGui::Text("Total: %d", static_cast(earned.size())); + ImGui::EndChild(); + } + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); }