2026-02-04 11:31:08 -08:00
|
|
|
#include "ui/spellbook_screen.hpp"
|
|
|
|
|
#include "core/input.hpp"
|
|
|
|
|
#include "core/application.hpp"
|
|
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
#include "pipeline/dbc_loader.hpp"
|
2026-02-06 16:04:25 -08:00
|
|
|
#include "pipeline/blp_loader.hpp"
|
2026-02-04 11:31:08 -08:00
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
|
|
|
|
namespace wowee { namespace ui {
|
|
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 11:31:08 -08:00
|
|
|
void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
|
|
|
|
if (dbcLoadAttempted) return;
|
|
|
|
|
dbcLoadAttempted = true;
|
|
|
|
|
|
|
|
|
|
if (!assetManager || !assetManager->isInitialized()) return;
|
|
|
|
|
|
|
|
|
|
auto dbc = assetManager->loadDBC("Spell.dbc");
|
|
|
|
|
if (!dbc || !dbc->isLoaded()) {
|
|
|
|
|
LOG_WARNING("Spellbook: Could not load Spell.dbc");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t fieldCount = dbc->getFieldCount();
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
if (fieldCount < 154) {
|
2026-02-04 11:31:08 -08:00
|
|
|
LOG_WARNING("Spellbook: Spell.dbc has ", fieldCount, " fields, expected 234+");
|
2026-02-06 16:04:25 -08:00
|
|
|
return;
|
2026-02-04 11:31:08 -08:00
|
|
|
}
|
|
|
|
|
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
// WoW 3.3.5a Spell.dbc fields (0-based):
|
|
|
|
|
// 0 = SpellID, 4 = Attributes, 133 = SpellIconID, 136 = SpellName_enUS, 153 = RankText_enUS
|
2026-02-04 11:31:08 -08:00
|
|
|
uint32_t count = dbc->getRecordCount();
|
|
|
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
|
|
|
uint32_t spellId = dbc->getUInt32(i, 0);
|
2026-02-06 16:04:25 -08:00
|
|
|
if (spellId == 0) continue;
|
|
|
|
|
|
|
|
|
|
SpellInfo info;
|
|
|
|
|
info.spellId = spellId;
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
info.attributes = dbc->getUInt32(i, 4);
|
2026-02-06 16:04:25 -08:00
|
|
|
info.iconId = dbc->getUInt32(i, 133);
|
|
|
|
|
info.name = dbc->getString(i, 136);
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
info.rank = dbc->getString(i, 153);
|
2026-02-06 16:04:25 -08:00
|
|
|
|
|
|
|
|
if (!info.name.empty()) {
|
|
|
|
|
spellData[spellId] = std::move(info);
|
2026-02-04 11:31:08 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dbcLoaded = true;
|
2026-02-06 16:04:25 -08:00
|
|
|
LOG_INFO("Spellbook: Loaded ", spellData.size(), " spells from Spell.dbc");
|
2026-02-04 11:31:08 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<uint32_t>& 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;
|
2026-02-04 11:31:08 -08:00
|
|
|
}
|
2026-02-06 16:04:25 -08:00
|
|
|
|
|
|
|
|
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;
|
2026-02-04 11:31:08 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager) {
|
|
|
|
|
// P key toggle (edge-triggered)
|
|
|
|
|
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
|
|
|
|
bool pDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_P);
|
|
|
|
|
if (pDown && !pKeyWasDown) {
|
|
|
|
|
open = !open;
|
|
|
|
|
}
|
|
|
|
|
pKeyWasDown = pDown;
|
|
|
|
|
|
|
|
|
|
if (!open) return;
|
|
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
// Lazy-load DBC data on first open
|
2026-02-04 11:31:08 -08:00
|
|
|
if (!dbcLoadAttempted) {
|
|
|
|
|
loadSpellDBC(assetManager);
|
2026-02-06 16:04:25 -08:00
|
|
|
loadSpellIconDBC(assetManager);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rebuild categories if spell list changed
|
|
|
|
|
const auto& spells = gameHandler.getKnownSpells();
|
|
|
|
|
if (spells.size() != lastKnownSpellCount) {
|
|
|
|
|
categorizeSpells(spells);
|
2026-02-04 11:31:08 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
float bookW = 360.0f;
|
|
|
|
|
float bookH = std::min(520.0f, screenH - 120.0f);
|
2026-02-04 11:31:08 -08:00
|
|
|
float bookX = screenW - bookW - 10.0f;
|
|
|
|
|
float bookY = 80.0f;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(bookX, bookY), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(bookW, bookH), ImGuiCond_FirstUseEver);
|
2026-02-06 20:29:59 -08:00
|
|
|
ImGui::SetNextWindowSizeConstraints(ImVec2(280, 200), ImVec2(screenW, screenH));
|
|
|
|
|
|
2026-02-04 11:31:08 -08:00
|
|
|
bool windowOpen = open;
|
2026-02-06 20:34:20 -08:00
|
|
|
if (ImGui::Begin("Spellbook", &windowOpen)) {
|
2026-02-06 20:29:59 -08:00
|
|
|
// Clamp window position to stay on screen
|
|
|
|
|
ImVec2 winPos = ImGui::GetWindowPos();
|
|
|
|
|
ImVec2 winSize = ImGui::GetWindowSize();
|
|
|
|
|
float clampedX = std::max(0.0f, std::min(winPos.x, screenW - winSize.x));
|
|
|
|
|
float clampedY = std::max(0.0f, std::min(winPos.y, screenH - winSize.y));
|
|
|
|
|
if (clampedX != winPos.x || clampedY != winPos.y) {
|
|
|
|
|
ImGui::SetWindowPos(ImVec2(clampedX, clampedY));
|
|
|
|
|
}
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
// Tab bar
|
|
|
|
|
if (ImGui::BeginTabBar("SpellbookTabs")) {
|
|
|
|
|
auto renderTab = [&](const char* label, SpellTab tab, const std::vector<const SpellInfo*>& spellList) {
|
|
|
|
|
if (ImGui::BeginTabItem(label)) {
|
|
|
|
|
currentTab = tab;
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
if (spellList.empty()) {
|
|
|
|
|
ImGui::TextDisabled("No spells in this category.");
|
|
|
|
|
}
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
// Spell list with icons
|
2026-02-06 20:27:01 -08:00
|
|
|
ImGui::BeginChild("SpellList", ImVec2(0, 0), true);
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
float iconSize = 32.0f;
|
|
|
|
|
bool isPassiveTab = (tab == SpellTab::PASSIVE);
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
for (const SpellInfo* info : spellList) {
|
|
|
|
|
ImGui::PushID(static_cast<int>(info->spellId));
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
float cd = gameHandler.getSpellCooldown(info->spellId);
|
|
|
|
|
bool onCooldown = cd > 0.0f;
|
2026-02-06 20:34:20 -08:00
|
|
|
bool isDim = isPassiveTab || onCooldown;
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
GLuint iconTex = getSpellIcon(info->iconId, assetManager);
|
|
|
|
|
|
2026-02-06 20:34:20 -08:00
|
|
|
// Selectable consumes clicks properly (prevents window drag)
|
|
|
|
|
ImGui::Selectable("##row", false,
|
|
|
|
|
ImGuiSelectableFlags_AllowDoubleClick, ImVec2(0, iconSize));
|
|
|
|
|
bool rowHovered = ImGui::IsItemHovered();
|
|
|
|
|
bool rowClicked = ImGui::IsItemClicked(0);
|
|
|
|
|
ImVec2 rMin = ImGui::GetItemRectMin();
|
|
|
|
|
auto* dl = ImGui::GetWindowDrawList();
|
|
|
|
|
|
|
|
|
|
// Draw icon on top of selectable
|
2026-02-06 16:04:25 -08:00
|
|
|
if (iconTex) {
|
2026-02-06 20:34:20 -08:00
|
|
|
dl->AddImage((ImTextureID)(uintptr_t)iconTex,
|
|
|
|
|
rMin, ImVec2(rMin.x + iconSize, rMin.y + iconSize));
|
2026-02-06 16:04:25 -08:00
|
|
|
} else {
|
2026-02-06 20:34:20 -08:00
|
|
|
dl->AddRectFilled(rMin,
|
|
|
|
|
ImVec2(rMin.x + iconSize, rMin.y + iconSize),
|
2026-02-06 16:04:25 -08:00
|
|
|
IM_COL32(60, 60, 80, 255));
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 20:34:20 -08:00
|
|
|
// Draw name and rank text
|
|
|
|
|
ImU32 textCol = isDim ? IM_COL32(153, 153, 153, 255)
|
|
|
|
|
: ImGui::GetColorU32(ImGuiCol_Text);
|
|
|
|
|
ImU32 dimCol = ImGui::GetColorU32(ImGuiCol_TextDisabled);
|
|
|
|
|
float textX = rMin.x + iconSize + 4.0f;
|
|
|
|
|
dl->AddText(ImVec2(textX, rMin.y), textCol, info->name.c_str());
|
2026-02-06 16:04:25 -08:00
|
|
|
if (!info->rank.empty()) {
|
2026-02-06 20:34:20 -08:00
|
|
|
dl->AddText(ImVec2(textX, rMin.y + ImGui::GetTextLineHeight()),
|
|
|
|
|
dimCol, info->rank.c_str());
|
2026-02-06 16:04:25 -08:00
|
|
|
} else if (onCooldown) {
|
2026-02-06 20:34:20 -08:00
|
|
|
char cdBuf[32];
|
|
|
|
|
snprintf(cdBuf, sizeof(cdBuf), "%.1fs cooldown", cd);
|
|
|
|
|
dl->AddText(ImVec2(textX, rMin.y + ImGui::GetTextLineHeight()),
|
|
|
|
|
dimCol, cdBuf);
|
2026-02-06 16:04:25 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 20:34:20 -08:00
|
|
|
if (rowHovered) {
|
|
|
|
|
// Start drag on click (not passive)
|
|
|
|
|
if (rowClicked && !isPassiveTab) {
|
2026-02-06 20:27:01 -08:00
|
|
|
draggingSpell_ = true;
|
|
|
|
|
dragSpellId_ = info->spellId;
|
|
|
|
|
dragSpellIconTex_ = iconTex;
|
2026-02-06 16:04:25 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsMouseDoubleClicked(0) && !isPassiveTab && !onCooldown) {
|
2026-02-06 20:27:01 -08:00
|
|
|
draggingSpell_ = false;
|
|
|
|
|
dragSpellId_ = 0;
|
|
|
|
|
dragSpellIconTex_ = 0;
|
2026-02-06 16:04:25 -08:00
|
|
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
gameHandler.castSpell(info->spellId, target);
|
|
|
|
|
}
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-02-06 20:27:01 -08:00
|
|
|
// Tooltip (only when not dragging)
|
|
|
|
|
if (!draggingSpell_) {
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
ImGui::Text("%s", info->name.c_str());
|
|
|
|
|
if (!info->rank.empty()) {
|
|
|
|
|
ImGui::TextDisabled("%s", info->rank.c_str());
|
2026-02-06 16:04:25 -08:00
|
|
|
}
|
2026-02-06 20:27:01 -08:00
|
|
|
ImGui::TextDisabled("Spell ID: %u", info->spellId);
|
|
|
|
|
if (isPassiveTab) {
|
|
|
|
|
ImGui::TextDisabled("Passive");
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextDisabled("Drag to action bar to assign");
|
|
|
|
|
if (!onCooldown) {
|
|
|
|
|
ImGui::TextDisabled("Double-click to cast");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndTooltip();
|
2026-02-06 16:04:25 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
2026-02-04 11:31:08 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
ImGui::EndChild();
|
|
|
|
|
ImGui::EndTabItem();
|
2026-02-04 11:31:08 -08:00
|
|
|
}
|
2026-02-06 16:04:25 -08:00
|
|
|
};
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
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());
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
renderTab(generalLabel, SpellTab::GENERAL, generalSpells);
|
|
|
|
|
renderTab(activeLabel, SpellTab::ACTIVE, activeSpells);
|
|
|
|
|
renderTab(passiveLabel, SpellTab::PASSIVE, passiveSpells);
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabBar();
|
|
|
|
|
}
|
2026-02-04 11:31:08 -08:00
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
if (!windowOpen) {
|
|
|
|
|
open = false;
|
|
|
|
|
}
|
2026-02-06 20:27:01 -08:00
|
|
|
|
|
|
|
|
// Render dragged spell icon at cursor
|
|
|
|
|
if (draggingSpell_ && dragSpellId_ != 0) {
|
|
|
|
|
ImVec2 mousePos = ImGui::GetMousePos();
|
|
|
|
|
float dragSize = 32.0f;
|
|
|
|
|
if (dragSpellIconTex_) {
|
|
|
|
|
ImGui::GetForegroundDrawList()->AddImage(
|
|
|
|
|
(ImTextureID)(uintptr_t)dragSpellIconTex_,
|
|
|
|
|
ImVec2(mousePos.x - dragSize * 0.5f, mousePos.y - dragSize * 0.5f),
|
|
|
|
|
ImVec2(mousePos.x + dragSize * 0.5f, mousePos.y + dragSize * 0.5f));
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::GetForegroundDrawList()->AddRectFilled(
|
|
|
|
|
ImVec2(mousePos.x - dragSize * 0.5f, mousePos.y - dragSize * 0.5f),
|
|
|
|
|
ImVec2(mousePos.x + dragSize * 0.5f, mousePos.y + dragSize * 0.5f),
|
|
|
|
|
IM_COL32(80, 80, 120, 180));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancel drag on mouse release (action bar consumes it before this if dropped on a slot)
|
|
|
|
|
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
|
|
|
|
draggingSpell_ = false;
|
|
|
|
|
dragSpellId_ = 0;
|
|
|
|
|
dragSpellIconTex_ = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-04 11:31:08 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}} // namespace wowee::ui
|