From caeb6f56f7b24bec11bc0ffdd6b065e5346a8bda Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Feb 2026 16:04:25 -0800 Subject: [PATCH] Fix hair/vendor/loot bugs, revamp spellbook with tabs and icons, clean up action bar, add talent placeholder - Fix white hair: always override M2 type-6 texture with DBC hair texture when available - Fix vendor sell: add sellPrice to ItemDef/ItemTemplateRow, use directly instead of empty cache - Fix empty loot: skip loot window when corpse has no items and no gold - Revamp spellbook (P key): tabbed UI (General/Active/Passive), spell icons from SpellIcon.dbc, rank text - Clean up action bar: only auto-populate Attack and Hearthstone, rest assigned via spellbook - Add talent placeholder (N key): 3-tab window with level/talent point display - Fix ffplay cleanup: non-blocking waitpid with SIGKILL fallback to prevent orphaned audio processes - Fix pre-existing getQualityColor visibility for loot window rendering --- CMakeLists.txt | 2 + include/game/inventory.hpp | 1 + include/platform/process.hpp | 13 +- include/ui/game_screen.hpp | 2 + include/ui/inventory_screen.hpp | 1 + include/ui/spellbook_screen.hpp | 38 +++- include/ui/talent_screen.hpp | 22 ++ src/core/application.cpp | 10 +- src/game/game_handler.cpp | 38 ++-- src/ui/game_screen.cpp | 3 + src/ui/spellbook_screen.cpp | 348 +++++++++++++++++++++++--------- src/ui/talent_screen.cpp | 67 ++++++ 12 files changed, 426 insertions(+), 119 deletions(-) create mode 100644 include/ui/talent_screen.hpp create mode 100644 src/ui/talent_screen.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c823ab06..79d35555 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -154,6 +154,7 @@ set(WOWEE_SOURCES src/ui/inventory_screen.cpp src/ui/quest_log_screen.cpp src/ui/spellbook_screen.cpp + src/ui/talent_screen.cpp # Main src/main.cpp @@ -241,6 +242,7 @@ set(WOWEE_HEADERS include/ui/game_screen.hpp include/ui/inventory_screen.hpp include/ui/spellbook_screen.hpp + include/ui/talent_screen.hpp ) # Create executable diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index 20bc1f23..e4edf8a3 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -42,6 +42,7 @@ struct ItemDef { int32_t intellect = 0; int32_t spirit = 0; uint32_t displayInfoId = 0; + uint32_t sellPrice = 0; }; struct ItemSlot { diff --git a/include/platform/process.hpp b/include/platform/process.hpp index c9871d49..039723d5 100644 --- a/include/platform/process.hpp +++ b/include/platform/process.hpp @@ -122,7 +122,18 @@ inline void killProcess(ProcessHandle& handle) { kill(-handle, SIGTERM); // kill process group kill(handle, SIGTERM); int status = 0; - waitpid(handle, &status, 0); + // Non-blocking wait with SIGKILL fallback after ~200ms + for (int i = 0; i < 20; ++i) { + pid_t ret = waitpid(handle, &status, WNOHANG); + if (ret != 0) break; // exited or error + usleep(10000); // 10ms + } + // If still alive, force kill + if (waitpid(handle, &status, WNOHANG) == 0) { + kill(-handle, SIGKILL); + kill(handle, SIGKILL); + waitpid(handle, &status, 0); + } #endif handle = INVALID_PROCESS; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index b426e313..8bd09b92 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -7,6 +7,7 @@ #include "ui/inventory_screen.hpp" #include "ui/quest_log_screen.hpp" #include "ui/spellbook_screen.hpp" +#include "ui/talent_screen.hpp" #include #include #include @@ -150,6 +151,7 @@ private: InventoryScreen inventoryScreen; QuestLogScreen questLogScreen; SpellbookScreen spellbookScreen; + TalentScreen talentScreen; rendering::WorldMap worldMap; bool actionSpellDbAttempted = false; diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index 5e4f012d..5970a108 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -126,6 +126,7 @@ private: game::EquipSlot getEquipSlotForType(uint8_t inventoryType, game::Inventory& inv); void renderHeldItem(); +public: static ImVec4 getQualityColor(game::ItemQuality quality); }; diff --git a/include/ui/spellbook_screen.hpp b/include/ui/spellbook_screen.hpp index 56ab906d..e2eaa5ca 100644 --- a/include/ui/spellbook_screen.hpp +++ b/include/ui/spellbook_screen.hpp @@ -1,8 +1,10 @@ #pragma once #include "game/game_handler.hpp" +#include #include #include +#include #include namespace wowee { @@ -11,6 +13,17 @@ namespace pipeline { class AssetManager; } namespace ui { +struct SpellInfo { + uint32_t spellId = 0; + std::string name; + std::string rank; + uint32_t iconId = 0; // SpellIconID + uint32_t attributes = 0; // Spell attributes (field 75) + bool isPassive() const { return (attributes & 0x40) != 0; } +}; + +enum class SpellTab { GENERAL, ACTIVE, PASSIVE }; + class SpellbookScreen { public: void render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager); @@ -22,16 +35,33 @@ private: bool open = false; bool pKeyWasDown = false; - // Spell name cache (loaded from Spell.dbc) + // Spell data (loaded from Spell.dbc) bool dbcLoaded = false; bool dbcLoadAttempted = false; - std::unordered_map spellNames; + std::unordered_map spellData; + + // Icon data (loaded from SpellIcon.dbc) + bool iconDbLoaded = false; + std::unordered_map spellIconPaths; // SpellIconID -> path + std::unordered_map spellIconCache; // SpellIconID -> GL texture + + // Categorized spell lists (rebuilt when spell list changes) + std::vector generalSpells; + std::vector activeSpells; + std::vector passiveSpells; + size_t lastKnownSpellCount = 0; + + // Tab state + SpellTab currentTab = SpellTab::GENERAL; // Action bar assignment - int assigningSlot = -1; // Which action bar slot is being assigned (-1 = none) + int assigningSlot = -1; void loadSpellDBC(pipeline::AssetManager* assetManager); - std::string getSpellName(uint32_t spellId) const; + void loadSpellIconDBC(pipeline::AssetManager* assetManager); + void categorizeSpells(const std::vector& knownSpells); + GLuint getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager); + const SpellInfo* getSpellInfo(uint32_t spellId) const; }; } // namespace ui diff --git a/include/ui/talent_screen.hpp b/include/ui/talent_screen.hpp new file mode 100644 index 00000000..e7e36220 --- /dev/null +++ b/include/ui/talent_screen.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "game/game_handler.hpp" +#include + +namespace wowee { +namespace ui { + +class TalentScreen { +public: + void render(game::GameHandler& gameHandler); + bool isOpen() const { return open; } + void toggle() { open = !open; } + void setOpen(bool o) { open = o; } + +private: + bool open = false; + bool nKeyWasDown = false; +}; + +} // namespace ui +} // namespace wowee diff --git a/src/core/application.cpp b/src/core/application.cpp index 1b781fe4..59ed8838 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -875,10 +875,12 @@ void Application::spawnPlayerCharacter() { for (auto& tex : model.textures) { if (tex.type == 1 && tex.filename.empty()) { tex.filename = bodySkinPath; - } else if (tex.type == 6 && tex.filename.empty()) { - tex.filename = hairTexturePath.empty() - ? "Character\\Human\\Hair00_00.blp" - : hairTexturePath; + } else if (tex.type == 6) { + if (!hairTexturePath.empty()) { + tex.filename = hairTexturePath; + } else if (tex.filename.empty()) { + tex.filename = "Character\\Human\\Hair00_00.blp"; + } } else if (tex.type == 8 && tex.filename.empty()) { if (!underwearPaths.empty()) { tex.filename = underwearPaths[0]; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 41ea983f..80d3691e 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -45,6 +45,7 @@ struct ItemTemplateRow { uint8_t quality = 0; uint8_t inventoryType = 0; int32_t maxStack = 1; + uint32_t sellPrice = 0; }; struct SinglePlayerLootDb { @@ -500,6 +501,7 @@ static SinglePlayerLootDb& getSinglePlayerLootDb() { int idxQuality = columnIndex(cols, "Quality"); int idxInvType = columnIndex(cols, "InventoryType"); int idxStack = columnIndex(cols, "stackable"); + int idxSellPrice = columnIndex(cols, "SellPrice"); if (idxEntry >= 0 && std::filesystem::exists(itemTemplatePath)) { std::ifstream in(itemTemplatePath); processInsertStatements(in, [&](const std::vector& row) { @@ -523,6 +525,9 @@ static SinglePlayerLootDb& getSinglePlayerLootDb() { ir.maxStack = static_cast(std::stol(row[idxStack])); if (ir.maxStack <= 0) ir.maxStack = 1; } + if (idxSellPrice >= 0 && idxSellPrice < static_cast(row.size())) { + ir.sellPrice = static_cast(std::stoul(row[idxSellPrice])); + } db.itemTemplates[ir.itemId] = std::move(ir); } catch (const std::exception&) { } @@ -1858,6 +1863,7 @@ void GameHandler::applySinglePlayerStartData(Race race, Class cls) { def.quality = static_cast(itTpl->second.quality); def.inventoryType = itTpl->second.inventoryType; def.maxStack = std::max(def.maxStack, static_cast(itTpl->second.maxStack)); + def.sellPrice = itTpl->second.sellPrice; } else { def.name = "Item " + std::to_string(row.itemId); } @@ -1915,15 +1921,7 @@ void GameHandler::applySinglePlayerStartData(Race race, Class cls) { } if (!hasActionRows) { - // Auto-populate action bar with known spells - int slot = 1; - for (uint32_t spellId : knownSpells) { - if (spellId == 6603 || spellId == 8690) continue; - if (slot >= 11) break; - actionBar[slot].type = ActionBarSlot::SPELL; - actionBar[slot].id = spellId; - slot++; - } + // Leave slots 1-10 empty; player assigns from spellbook } markSinglePlayerDirty(SP_DIRTY_INVENTORY | SP_DIRTY_SPELLS | SP_DIRTY_ACTIONBAR | @@ -3365,18 +3363,11 @@ void GameHandler::handleInitialSpells(network::Packet& packet) { } } - // Auto-populate action bar: Attack in slot 1, Hearthstone in slot 12, rest filled with known spells + // Auto-populate action bar: Attack in slot 1, Hearthstone in slot 12 actionBar[0].type = ActionBarSlot::SPELL; actionBar[0].id = 6603; // Attack actionBar[11].type = ActionBarSlot::SPELL; actionBar[11].id = 8690; // Hearthstone - int slot = 1; - for (int i = 0; i < static_cast(knownSpells.size()) && slot < 11; ++i) { - if (knownSpells[i] == 6603 || knownSpells[i] == 8690) continue; - actionBar[slot].type = ActionBarSlot::SPELL; - actionBar[slot].id = knownSpells[i]; - slot++; - } LOG_INFO("Learned ", knownSpells.size(), " spells"); } @@ -3610,6 +3601,10 @@ void GameHandler::lootTarget(uint64_t guid) { state.data = generateLocalLoot(guid); it = localLootState_.emplace(guid, std::move(state)).first; } + if (it->second.data.items.empty() && it->second.data.gold == 0) { + addSystemChatMessage("No loot."); + return; + } simulateLootResponse(it->second.data); return; } @@ -3640,6 +3635,7 @@ void GameHandler::lootItem(uint8_t slotIndex) { def.quality = static_cast(itTpl->second.quality); def.inventoryType = itTpl->second.inventoryType; def.maxStack = std::max(def.maxStack, static_cast(itTpl->second.maxStack)); + def.sellPrice = itTpl->second.sellPrice; } else { def.name = "Item " + std::to_string(it->itemId); } @@ -3801,11 +3797,13 @@ void GameHandler::sellItemBySlot(int backpackIndex) { if (slot.empty()) return; if (singlePlayerMode_) { - auto it = itemInfoCache_.find(slot.item.itemId); - if (it != itemInfoCache_.end() && it->second.sellPrice > 0) { - addMoneyCopper(it->second.sellPrice); + if (slot.item.sellPrice > 0) { + addMoneyCopper(slot.item.sellPrice); std::string msg = "You sold " + slot.item.name + "."; addSystemChatMessage(msg); + } else { + addSystemChatMessage("You can't sell " + slot.item.name + "."); + return; } inventory.clearBackpackSlot(backpackIndex); notifyInventoryChanged(); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 9f80e52c..0476eefd 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -103,6 +103,9 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Spellbook (P key toggle handled inside) spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager()); + // Talents (N key toggle handled inside) + talentScreen.render(gameHandler); + // Set up inventory screen asset manager + player appearance (once) { static bool inventoryScreenInit = false; diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index b233b7bf..0f95f030 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -3,11 +3,26 @@ #include "core/application.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_loader.hpp" +#include "pipeline/blp_loader.hpp" #include "core/logger.hpp" #include namespace wowee { namespace ui { +// General utility spells that belong in the General tab +static bool isGeneralSpell(uint32_t spellId) { + switch (spellId) { + case 6603: // Attack + case 8690: // Hearthstone + case 3365: // Opening + case 21651: // Opening + case 21652: // Closing + return true; + default: + return false; + } +} + void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) { if (dbcLoadAttempted) return; dbcLoadAttempted = true; @@ -20,42 +35,129 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) { return; } - // WoW 3.3.5a Spell.dbc: field 0 = SpellID, field 136 = SpellName_enUS - // Validate field count to determine name field index uint32_t fieldCount = dbc->getFieldCount(); - uint32_t nameField = 136; - - if (fieldCount < 137) { + if (fieldCount < 142) { LOG_WARNING("Spellbook: Spell.dbc has ", fieldCount, " fields, expected 234+"); - // Try a heuristic: for smaller DBCs, name might be elsewhere - if (fieldCount > 10) { - nameField = fieldCount > 140 ? 136 : 1; - } else { - return; - } + return; } + // WoW 3.3.5a Spell.dbc fields: + // 0 = SpellID, 75 = Attributes, 133 = SpellIconID, 136 = SpellName, 141 = RankText uint32_t count = dbc->getRecordCount(); for (uint32_t i = 0; i < count; ++i) { uint32_t spellId = dbc->getUInt32(i, 0); - std::string name = dbc->getString(i, nameField); - if (!name.empty() && spellId > 0) { - spellNames[spellId] = name; + if (spellId == 0) continue; + + SpellInfo info; + info.spellId = spellId; + info.attributes = dbc->getUInt32(i, 75); + info.iconId = dbc->getUInt32(i, 133); + info.name = dbc->getString(i, 136); + info.rank = dbc->getString(i, 141); + + if (!info.name.empty()) { + spellData[spellId] = std::move(info); } } dbcLoaded = true; - LOG_INFO("Spellbook: Loaded ", spellNames.size(), " spell names from Spell.dbc"); + LOG_INFO("Spellbook: Loaded ", spellData.size(), " spells from Spell.dbc"); } -std::string SpellbookScreen::getSpellName(uint32_t spellId) const { - auto it = spellNames.find(spellId); - if (it != spellNames.end()) { - return it->second; +void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) { + if (iconDbLoaded) return; + iconDbLoaded = true; + + if (!assetManager || !assetManager->isInitialized()) return; + + auto dbc = assetManager->loadDBC("SpellIcon.dbc"); + if (!dbc || !dbc->isLoaded()) { + LOG_WARNING("Spellbook: Could not load SpellIcon.dbc"); + return; } - char buf[32]; - snprintf(buf, sizeof(buf), "Spell #%u", spellId); - return buf; + + for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { + uint32_t id = dbc->getUInt32(i, 0); + std::string path = dbc->getString(i, 1); + if (!path.empty() && id > 0) { + spellIconPaths[id] = path; + } + } + + LOG_INFO("Spellbook: Loaded ", spellIconPaths.size(), " spell icon paths"); +} + +void SpellbookScreen::categorizeSpells(const std::vector& knownSpells) { + generalSpells.clear(); + activeSpells.clear(); + passiveSpells.clear(); + + for (uint32_t spellId : knownSpells) { + auto it = spellData.find(spellId); + if (it == spellData.end()) continue; + + const SpellInfo* info = &it->second; + if (isGeneralSpell(spellId)) { + generalSpells.push_back(info); + } else if (info->isPassive()) { + passiveSpells.push_back(info); + } else { + activeSpells.push_back(info); + } + } + + // Sort each tab alphabetically + auto byName = [](const SpellInfo* a, const SpellInfo* b) { return a->name < b->name; }; + std::sort(generalSpells.begin(), generalSpells.end(), byName); + std::sort(activeSpells.begin(), activeSpells.end(), byName); + std::sort(passiveSpells.begin(), passiveSpells.end(), byName); + + lastKnownSpellCount = knownSpells.size(); +} + +GLuint SpellbookScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) { + if (iconId == 0 || !assetManager) return 0; + + auto cit = spellIconCache.find(iconId); + if (cit != spellIconCache.end()) return cit->second; + + auto pit = spellIconPaths.find(iconId); + if (pit == spellIconPaths.end()) { + spellIconCache[iconId] = 0; + return 0; + } + + std::string iconPath = pit->second + ".blp"; + auto blpData = assetManager->readFile(iconPath); + if (blpData.empty()) { + spellIconCache[iconId] = 0; + return 0; + } + + auto image = pipeline::BLPLoader::load(blpData); + if (!image.isValid()) { + spellIconCache[iconId] = 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[iconId] = texId; + return texId; +} + +const SpellInfo* SpellbookScreen::getSpellInfo(uint32_t spellId) const { + auto it = spellData.find(spellId); + return (it != spellData.end()) ? &it->second : nullptr; } void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager) { @@ -69,17 +171,24 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana if (!open) return; - // Lazy-load Spell.dbc on first open + // Lazy-load DBC data on first open if (!dbcLoadAttempted) { loadSpellDBC(assetManager); + loadSpellIconDBC(assetManager); + } + + // Rebuild categories if spell list changed + const auto& spells = gameHandler.getKnownSpells(); + if (spells.size() != lastKnownSpellCount) { + categorizeSpells(spells); } auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; - float bookW = 340.0f; - float bookH = std::min(500.0f, screenH - 120.0f); + float bookW = 360.0f; + float bookH = std::min(520.0f, screenH - 120.0f); float bookX = screenW - bookW - 10.0f; float bookY = 80.0f; @@ -88,82 +197,141 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana bool windowOpen = open; if (ImGui::Begin("Spellbook", &windowOpen)) { - const auto& spells = gameHandler.getKnownSpells(); - if (spells.empty()) { - ImGui::TextDisabled("No spells known."); - } else { - ImGui::Text("%zu spells known", spells.size()); - ImGui::Separator(); + // Tab bar + if (ImGui::BeginTabBar("SpellbookTabs")) { + auto renderTab = [&](const char* label, SpellTab tab, const std::vector& spellList) { + if (ImGui::BeginTabItem(label)) { + currentTab = tab; - // Action bar assignment mode indicator - if (assigningSlot >= 0) { - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), - "Click a spell to assign to slot %d", assigningSlot + 1); - if (ImGui::SmallButton("Cancel")) { - assigningSlot = -1; - } - ImGui::Separator(); - } - - // Spell list - ImGui::BeginChild("SpellList", ImVec2(0, -60), true); - - for (uint32_t spellId : spells) { - ImGui::PushID(static_cast(spellId)); - - std::string name = getSpellName(spellId); - float cd = gameHandler.getSpellCooldown(spellId); - bool onCooldown = cd > 0.0f; - - // Color based on state - if (onCooldown) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); - } - - // Spell entry - clickable - char label[256]; - if (onCooldown) { - snprintf(label, sizeof(label), "%s (%.1fs)", name.c_str(), cd); - } else { - snprintf(label, sizeof(label), "%s", name.c_str()); - } - - if (ImGui::Selectable(label, false, ImGuiSelectableFlags_AllowDoubleClick)) { + // Action bar assignment mode indicator if (assigningSlot >= 0) { - // Assign to action bar slot - gameHandler.setActionBarSlot(assigningSlot, - game::ActionBarSlot::SPELL, spellId); - assigningSlot = -1; - } else if (ImGui::IsMouseDoubleClicked(0)) { - // Double-click to cast - uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; - gameHandler.castSpell(spellId, target); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), + "Click a spell to assign to slot %d", assigningSlot + 1); + if (ImGui::SmallButton("Cancel")) { + assigningSlot = -1; + } + ImGui::Separator(); } - } - // Tooltip with spell ID - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::Text("%s", name.c_str()); - ImGui::TextDisabled("Spell ID: %u", spellId); - if (!onCooldown) { - ImGui::TextDisabled("Double-click to cast"); - ImGui::TextDisabled("Use action bar buttons below to assign"); + if (spellList.empty()) { + ImGui::TextDisabled("No spells in this category."); } - ImGui::EndTooltip(); + + // Spell list with icons + ImGui::BeginChild("SpellList", ImVec2(0, -60), true); + + float iconSize = 32.0f; + bool isPassiveTab = (tab == SpellTab::PASSIVE); + + for (const SpellInfo* info : spellList) { + ImGui::PushID(static_cast(info->spellId)); + + float cd = gameHandler.getSpellCooldown(info->spellId); + bool onCooldown = cd > 0.0f; + + // Dimmer for passive or cooldown spells + if (isPassiveTab || onCooldown) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + } + + // Icon + GLuint iconTex = getSpellIcon(info->iconId, assetManager); + float startY = ImGui::GetCursorPosY(); + + if (iconTex) { + ImGui::Image((ImTextureID)(uintptr_t)iconTex, + ImVec2(iconSize, iconSize)); + } else { + // Placeholder colored square + ImVec2 pos = ImGui::GetCursorScreenPos(); + ImGui::GetWindowDrawList()->AddRectFilled( + pos, ImVec2(pos.x + iconSize, pos.y + iconSize), + IM_COL32(60, 60, 80, 255)); + ImGui::Dummy(ImVec2(iconSize, iconSize)); + } + + ImGui::SameLine(); + + // Name and rank text + ImGui::BeginGroup(); + ImGui::Text("%s", info->name.c_str()); + if (!info->rank.empty()) { + ImGui::TextDisabled("%s", info->rank.c_str()); + } else if (onCooldown) { + ImGui::TextDisabled("%.1fs cooldown", cd); + } + ImGui::EndGroup(); + + // Make the whole row clickable + ImVec2 rowMin = ImVec2(ImGui::GetWindowPos().x, + ImGui::GetWindowPos().y + startY - ImGui::GetScrollY()); + ImVec2 rowMax = ImVec2(rowMin.x + ImGui::GetContentRegionAvail().x, + rowMin.y + std::max(iconSize, ImGui::GetCursorPosY() - startY)); + + if (ImGui::IsMouseHoveringRect(rowMin, rowMax) && ImGui::IsWindowHovered()) { + // Highlight + ImGui::GetWindowDrawList()->AddRectFilled( + rowMin, rowMax, IM_COL32(255, 255, 255, 20)); + + if (ImGui::IsMouseClicked(0)) { + if (assigningSlot >= 0 && !isPassiveTab) { + gameHandler.setActionBarSlot(assigningSlot, + game::ActionBarSlot::SPELL, info->spellId); + assigningSlot = -1; + } + } + + if (ImGui::IsMouseDoubleClicked(0) && !isPassiveTab && !onCooldown) { + uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; + gameHandler.castSpell(info->spellId, target); + } + + // Tooltip + ImGui::BeginTooltip(); + ImGui::Text("%s", info->name.c_str()); + if (!info->rank.empty()) { + ImGui::TextDisabled("%s", info->rank.c_str()); + } + ImGui::TextDisabled("Spell ID: %u", info->spellId); + if (isPassiveTab) { + ImGui::TextDisabled("Passive"); + } else { + if (!onCooldown) { + ImGui::TextDisabled("Double-click to cast"); + } + ImGui::TextDisabled("Use buttons below to assign to action bar"); + } + ImGui::EndTooltip(); + } + + if (isPassiveTab || onCooldown) { + ImGui::PopStyleColor(); + } + + ImGui::Spacing(); + ImGui::PopID(); + } + + ImGui::EndChild(); + ImGui::EndTabItem(); } + }; - if (onCooldown) { - ImGui::PopStyleColor(); - } + char generalLabel[32], activeLabel[32], passiveLabel[32]; + snprintf(generalLabel, sizeof(generalLabel), "General (%zu)", generalSpells.size()); + snprintf(activeLabel, sizeof(activeLabel), "Active (%zu)", activeSpells.size()); + snprintf(passiveLabel, sizeof(passiveLabel), "Passive (%zu)", passiveSpells.size()); - ImGui::PopID(); - } + renderTab(generalLabel, SpellTab::GENERAL, generalSpells); + renderTab(activeLabel, SpellTab::ACTIVE, activeSpells); + renderTab(passiveLabel, SpellTab::PASSIVE, passiveSpells); - ImGui::EndChild(); + ImGui::EndTabBar(); + } - // Action bar quick-assign buttons + // Action bar quick-assign buttons (not for passive tab) + if (currentTab != SpellTab::PASSIVE) { ImGui::Separator(); ImGui::Text("Assign to:"); ImGui::SameLine(); diff --git a/src/ui/talent_screen.cpp b/src/ui/talent_screen.cpp new file mode 100644 index 00000000..bd824e97 --- /dev/null +++ b/src/ui/talent_screen.cpp @@ -0,0 +1,67 @@ +#include "ui/talent_screen.hpp" +#include "core/input.hpp" +#include "core/application.hpp" + +namespace wowee { namespace ui { + +void TalentScreen::render(game::GameHandler& gameHandler) { + // N key toggle (edge-triggered) + bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard; + bool nDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_N); + if (nDown && !nKeyWasDown) { + open = !open; + } + nKeyWasDown = nDown; + + if (!open) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + float winW = 400.0f; + float winH = 450.0f; + float winX = (screenW - winW) * 0.5f; + float winY = (screenH - winH) * 0.5f; + + ImGui::SetNextWindowPos(ImVec2(winX, winY), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(winW, winH), ImGuiCond_FirstUseEver); + + bool windowOpen = open; + if (ImGui::Begin("Talents", &windowOpen)) { + // Placeholder tabs + if (ImGui::BeginTabBar("TalentTabs")) { + if (ImGui::BeginTabItem("Spec 1")) { + ImGui::Spacing(); + ImGui::TextDisabled("Talents coming soon."); + ImGui::Spacing(); + ImGui::TextDisabled("Talent trees will be implemented in a future update."); + + uint32_t level = gameHandler.getPlayerLevel(); + uint32_t talentPoints = (level >= 10) ? (level - 9) : 0; + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Text("Level: %u", level); + ImGui::Text("Talent points available: %u", talentPoints); + + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Spec 2")) { + ImGui::TextDisabled("Talents coming soon."); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Spec 3")) { + ImGui::TextDisabled("Talents coming soon."); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + } + ImGui::End(); + + if (!windowOpen) { + open = false; + } +} + +}} // namespace wowee::ui