mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
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
This commit is contained in:
parent
deabe0bedd
commit
c8986df836
12 changed files with 426 additions and 119 deletions
|
|
@ -154,6 +154,7 @@ set(WOWEE_SOURCES
|
||||||
src/ui/inventory_screen.cpp
|
src/ui/inventory_screen.cpp
|
||||||
src/ui/quest_log_screen.cpp
|
src/ui/quest_log_screen.cpp
|
||||||
src/ui/spellbook_screen.cpp
|
src/ui/spellbook_screen.cpp
|
||||||
|
src/ui/talent_screen.cpp
|
||||||
|
|
||||||
# Main
|
# Main
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
|
|
@ -241,6 +242,7 @@ set(WOWEE_HEADERS
|
||||||
include/ui/game_screen.hpp
|
include/ui/game_screen.hpp
|
||||||
include/ui/inventory_screen.hpp
|
include/ui/inventory_screen.hpp
|
||||||
include/ui/spellbook_screen.hpp
|
include/ui/spellbook_screen.hpp
|
||||||
|
include/ui/talent_screen.hpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create executable
|
# Create executable
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ struct ItemDef {
|
||||||
int32_t intellect = 0;
|
int32_t intellect = 0;
|
||||||
int32_t spirit = 0;
|
int32_t spirit = 0;
|
||||||
uint32_t displayInfoId = 0;
|
uint32_t displayInfoId = 0;
|
||||||
|
uint32_t sellPrice = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ItemSlot {
|
struct ItemSlot {
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,18 @@ inline void killProcess(ProcessHandle& handle) {
|
||||||
kill(-handle, SIGTERM); // kill process group
|
kill(-handle, SIGTERM); // kill process group
|
||||||
kill(handle, SIGTERM);
|
kill(handle, SIGTERM);
|
||||||
int status = 0;
|
int 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);
|
waitpid(handle, &status, 0);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
handle = INVALID_PROCESS;
|
handle = INVALID_PROCESS;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include "ui/inventory_screen.hpp"
|
#include "ui/inventory_screen.hpp"
|
||||||
#include "ui/quest_log_screen.hpp"
|
#include "ui/quest_log_screen.hpp"
|
||||||
#include "ui/spellbook_screen.hpp"
|
#include "ui/spellbook_screen.hpp"
|
||||||
|
#include "ui/talent_screen.hpp"
|
||||||
#include <GL/glew.h>
|
#include <GL/glew.h>
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -150,6 +151,7 @@ private:
|
||||||
InventoryScreen inventoryScreen;
|
InventoryScreen inventoryScreen;
|
||||||
QuestLogScreen questLogScreen;
|
QuestLogScreen questLogScreen;
|
||||||
SpellbookScreen spellbookScreen;
|
SpellbookScreen spellbookScreen;
|
||||||
|
TalentScreen talentScreen;
|
||||||
rendering::WorldMap worldMap;
|
rendering::WorldMap worldMap;
|
||||||
|
|
||||||
bool actionSpellDbAttempted = false;
|
bool actionSpellDbAttempted = false;
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@ private:
|
||||||
game::EquipSlot getEquipSlotForType(uint8_t inventoryType, game::Inventory& inv);
|
game::EquipSlot getEquipSlotForType(uint8_t inventoryType, game::Inventory& inv);
|
||||||
void renderHeldItem();
|
void renderHeldItem();
|
||||||
|
|
||||||
|
public:
|
||||||
static ImVec4 getQualityColor(game::ItemQuality quality);
|
static ImVec4 getQualityColor(game::ItemQuality quality);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "game/game_handler.hpp"
|
#include "game/game_handler.hpp"
|
||||||
|
#include <GL/glew.h>
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
@ -11,6 +13,17 @@ namespace pipeline { class AssetManager; }
|
||||||
|
|
||||||
namespace ui {
|
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 {
|
class SpellbookScreen {
|
||||||
public:
|
public:
|
||||||
void render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager);
|
void render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager);
|
||||||
|
|
@ -22,16 +35,33 @@ private:
|
||||||
bool open = false;
|
bool open = false;
|
||||||
bool pKeyWasDown = false;
|
bool pKeyWasDown = false;
|
||||||
|
|
||||||
// Spell name cache (loaded from Spell.dbc)
|
// Spell data (loaded from Spell.dbc)
|
||||||
bool dbcLoaded = false;
|
bool dbcLoaded = false;
|
||||||
bool dbcLoadAttempted = false;
|
bool dbcLoadAttempted = false;
|
||||||
std::unordered_map<uint32_t, std::string> spellNames;
|
std::unordered_map<uint32_t, SpellInfo> spellData;
|
||||||
|
|
||||||
|
// Icon data (loaded from SpellIcon.dbc)
|
||||||
|
bool iconDbLoaded = false;
|
||||||
|
std::unordered_map<uint32_t, std::string> spellIconPaths; // SpellIconID -> path
|
||||||
|
std::unordered_map<uint32_t, GLuint> spellIconCache; // SpellIconID -> GL texture
|
||||||
|
|
||||||
|
// Categorized spell lists (rebuilt when spell list changes)
|
||||||
|
std::vector<const SpellInfo*> generalSpells;
|
||||||
|
std::vector<const SpellInfo*> activeSpells;
|
||||||
|
std::vector<const SpellInfo*> passiveSpells;
|
||||||
|
size_t lastKnownSpellCount = 0;
|
||||||
|
|
||||||
|
// Tab state
|
||||||
|
SpellTab currentTab = SpellTab::GENERAL;
|
||||||
|
|
||||||
// Action bar assignment
|
// Action bar assignment
|
||||||
int assigningSlot = -1; // Which action bar slot is being assigned (-1 = none)
|
int assigningSlot = -1;
|
||||||
|
|
||||||
void loadSpellDBC(pipeline::AssetManager* assetManager);
|
void loadSpellDBC(pipeline::AssetManager* assetManager);
|
||||||
std::string getSpellName(uint32_t spellId) const;
|
void loadSpellIconDBC(pipeline::AssetManager* assetManager);
|
||||||
|
void categorizeSpells(const std::vector<uint32_t>& knownSpells);
|
||||||
|
GLuint getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager);
|
||||||
|
const SpellInfo* getSpellInfo(uint32_t spellId) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|
|
||||||
22
include/ui/talent_screen.hpp
Normal file
22
include/ui/talent_screen.hpp
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "game/game_handler.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -875,10 +875,12 @@ void Application::spawnPlayerCharacter() {
|
||||||
for (auto& tex : model.textures) {
|
for (auto& tex : model.textures) {
|
||||||
if (tex.type == 1 && tex.filename.empty()) {
|
if (tex.type == 1 && tex.filename.empty()) {
|
||||||
tex.filename = bodySkinPath;
|
tex.filename = bodySkinPath;
|
||||||
} else if (tex.type == 6 && tex.filename.empty()) {
|
} else if (tex.type == 6) {
|
||||||
tex.filename = hairTexturePath.empty()
|
if (!hairTexturePath.empty()) {
|
||||||
? "Character\\Human\\Hair00_00.blp"
|
tex.filename = hairTexturePath;
|
||||||
: hairTexturePath;
|
} else if (tex.filename.empty()) {
|
||||||
|
tex.filename = "Character\\Human\\Hair00_00.blp";
|
||||||
|
}
|
||||||
} else if (tex.type == 8 && tex.filename.empty()) {
|
} else if (tex.type == 8 && tex.filename.empty()) {
|
||||||
if (!underwearPaths.empty()) {
|
if (!underwearPaths.empty()) {
|
||||||
tex.filename = underwearPaths[0];
|
tex.filename = underwearPaths[0];
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ struct ItemTemplateRow {
|
||||||
uint8_t quality = 0;
|
uint8_t quality = 0;
|
||||||
uint8_t inventoryType = 0;
|
uint8_t inventoryType = 0;
|
||||||
int32_t maxStack = 1;
|
int32_t maxStack = 1;
|
||||||
|
uint32_t sellPrice = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SinglePlayerLootDb {
|
struct SinglePlayerLootDb {
|
||||||
|
|
@ -500,6 +501,7 @@ static SinglePlayerLootDb& getSinglePlayerLootDb() {
|
||||||
int idxQuality = columnIndex(cols, "Quality");
|
int idxQuality = columnIndex(cols, "Quality");
|
||||||
int idxInvType = columnIndex(cols, "InventoryType");
|
int idxInvType = columnIndex(cols, "InventoryType");
|
||||||
int idxStack = columnIndex(cols, "stackable");
|
int idxStack = columnIndex(cols, "stackable");
|
||||||
|
int idxSellPrice = columnIndex(cols, "SellPrice");
|
||||||
if (idxEntry >= 0 && std::filesystem::exists(itemTemplatePath)) {
|
if (idxEntry >= 0 && std::filesystem::exists(itemTemplatePath)) {
|
||||||
std::ifstream in(itemTemplatePath);
|
std::ifstream in(itemTemplatePath);
|
||||||
processInsertStatements(in, [&](const std::vector<std::string>& row) {
|
processInsertStatements(in, [&](const std::vector<std::string>& row) {
|
||||||
|
|
@ -523,6 +525,9 @@ static SinglePlayerLootDb& getSinglePlayerLootDb() {
|
||||||
ir.maxStack = static_cast<int32_t>(std::stol(row[idxStack]));
|
ir.maxStack = static_cast<int32_t>(std::stol(row[idxStack]));
|
||||||
if (ir.maxStack <= 0) ir.maxStack = 1;
|
if (ir.maxStack <= 0) ir.maxStack = 1;
|
||||||
}
|
}
|
||||||
|
if (idxSellPrice >= 0 && idxSellPrice < static_cast<int>(row.size())) {
|
||||||
|
ir.sellPrice = static_cast<uint32_t>(std::stoul(row[idxSellPrice]));
|
||||||
|
}
|
||||||
db.itemTemplates[ir.itemId] = std::move(ir);
|
db.itemTemplates[ir.itemId] = std::move(ir);
|
||||||
} catch (const std::exception&) {
|
} catch (const std::exception&) {
|
||||||
}
|
}
|
||||||
|
|
@ -1858,6 +1863,7 @@ void GameHandler::applySinglePlayerStartData(Race race, Class cls) {
|
||||||
def.quality = static_cast<ItemQuality>(itTpl->second.quality);
|
def.quality = static_cast<ItemQuality>(itTpl->second.quality);
|
||||||
def.inventoryType = itTpl->second.inventoryType;
|
def.inventoryType = itTpl->second.inventoryType;
|
||||||
def.maxStack = std::max(def.maxStack, static_cast<uint32_t>(itTpl->second.maxStack));
|
def.maxStack = std::max(def.maxStack, static_cast<uint32_t>(itTpl->second.maxStack));
|
||||||
|
def.sellPrice = itTpl->second.sellPrice;
|
||||||
} else {
|
} else {
|
||||||
def.name = "Item " + std::to_string(row.itemId);
|
def.name = "Item " + std::to_string(row.itemId);
|
||||||
}
|
}
|
||||||
|
|
@ -1915,15 +1921,7 @@ void GameHandler::applySinglePlayerStartData(Race race, Class cls) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasActionRows) {
|
if (!hasActionRows) {
|
||||||
// Auto-populate action bar with known spells
|
// Leave slots 1-10 empty; player assigns from spellbook
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
markSinglePlayerDirty(SP_DIRTY_INVENTORY | SP_DIRTY_SPELLS | SP_DIRTY_ACTIONBAR |
|
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].type = ActionBarSlot::SPELL;
|
||||||
actionBar[0].id = 6603; // Attack
|
actionBar[0].id = 6603; // Attack
|
||||||
actionBar[11].type = ActionBarSlot::SPELL;
|
actionBar[11].type = ActionBarSlot::SPELL;
|
||||||
actionBar[11].id = 8690; // Hearthstone
|
actionBar[11].id = 8690; // Hearthstone
|
||||||
int slot = 1;
|
|
||||||
for (int i = 0; i < static_cast<int>(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");
|
LOG_INFO("Learned ", knownSpells.size(), " spells");
|
||||||
}
|
}
|
||||||
|
|
@ -3610,6 +3601,10 @@ void GameHandler::lootTarget(uint64_t guid) {
|
||||||
state.data = generateLocalLoot(guid);
|
state.data = generateLocalLoot(guid);
|
||||||
it = localLootState_.emplace(guid, std::move(state)).first;
|
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);
|
simulateLootResponse(it->second.data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -3640,6 +3635,7 @@ void GameHandler::lootItem(uint8_t slotIndex) {
|
||||||
def.quality = static_cast<ItemQuality>(itTpl->second.quality);
|
def.quality = static_cast<ItemQuality>(itTpl->second.quality);
|
||||||
def.inventoryType = itTpl->second.inventoryType;
|
def.inventoryType = itTpl->second.inventoryType;
|
||||||
def.maxStack = std::max(def.maxStack, static_cast<uint32_t>(itTpl->second.maxStack));
|
def.maxStack = std::max(def.maxStack, static_cast<uint32_t>(itTpl->second.maxStack));
|
||||||
|
def.sellPrice = itTpl->second.sellPrice;
|
||||||
} else {
|
} else {
|
||||||
def.name = "Item " + std::to_string(it->itemId);
|
def.name = "Item " + std::to_string(it->itemId);
|
||||||
}
|
}
|
||||||
|
|
@ -3801,11 +3797,13 @@ void GameHandler::sellItemBySlot(int backpackIndex) {
|
||||||
if (slot.empty()) return;
|
if (slot.empty()) return;
|
||||||
|
|
||||||
if (singlePlayerMode_) {
|
if (singlePlayerMode_) {
|
||||||
auto it = itemInfoCache_.find(slot.item.itemId);
|
if (slot.item.sellPrice > 0) {
|
||||||
if (it != itemInfoCache_.end() && it->second.sellPrice > 0) {
|
addMoneyCopper(slot.item.sellPrice);
|
||||||
addMoneyCopper(it->second.sellPrice);
|
|
||||||
std::string msg = "You sold " + slot.item.name + ".";
|
std::string msg = "You sold " + slot.item.name + ".";
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
|
} else {
|
||||||
|
addSystemChatMessage("You can't sell " + slot.item.name + ".");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
inventory.clearBackpackSlot(backpackIndex);
|
inventory.clearBackpackSlot(backpackIndex);
|
||||||
notifyInventoryChanged();
|
notifyInventoryChanged();
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,9 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
// Spellbook (P key toggle handled inside)
|
// Spellbook (P key toggle handled inside)
|
||||||
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
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)
|
// Set up inventory screen asset manager + player appearance (once)
|
||||||
{
|
{
|
||||||
static bool inventoryScreenInit = false;
|
static bool inventoryScreenInit = false;
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,26 @@
|
||||||
#include "core/application.hpp"
|
#include "core/application.hpp"
|
||||||
#include "pipeline/asset_manager.hpp"
|
#include "pipeline/asset_manager.hpp"
|
||||||
#include "pipeline/dbc_loader.hpp"
|
#include "pipeline/dbc_loader.hpp"
|
||||||
|
#include "pipeline/blp_loader.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
namespace wowee { namespace ui {
|
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) {
|
void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
||||||
if (dbcLoadAttempted) return;
|
if (dbcLoadAttempted) return;
|
||||||
dbcLoadAttempted = true;
|
dbcLoadAttempted = true;
|
||||||
|
|
@ -20,42 +35,129 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
||||||
return;
|
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 fieldCount = dbc->getFieldCount();
|
||||||
uint32_t nameField = 136;
|
if (fieldCount < 142) {
|
||||||
|
|
||||||
if (fieldCount < 137) {
|
|
||||||
LOG_WARNING("Spellbook: Spell.dbc has ", fieldCount, " fields, expected 234+");
|
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();
|
uint32_t count = dbc->getRecordCount();
|
||||||
for (uint32_t i = 0; i < count; ++i) {
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
uint32_t spellId = dbc->getUInt32(i, 0);
|
uint32_t spellId = dbc->getUInt32(i, 0);
|
||||||
std::string name = dbc->getString(i, nameField);
|
if (spellId == 0) continue;
|
||||||
if (!name.empty() && spellId > 0) {
|
|
||||||
spellNames[spellId] = name;
|
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;
|
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 {
|
void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
|
||||||
auto it = spellNames.find(spellId);
|
if (iconDbLoaded) return;
|
||||||
if (it != spellNames.end()) {
|
iconDbLoaded = true;
|
||||||
return it->second;
|
|
||||||
|
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);
|
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||||
return buf;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager) {
|
||||||
|
|
@ -69,17 +171,24 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana
|
||||||
|
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
// Lazy-load Spell.dbc on first open
|
// Lazy-load DBC data on first open
|
||||||
if (!dbcLoadAttempted) {
|
if (!dbcLoadAttempted) {
|
||||||
loadSpellDBC(assetManager);
|
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();
|
auto* window = core::Application::getInstance().getWindow();
|
||||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||||
|
|
||||||
float bookW = 340.0f;
|
float bookW = 360.0f;
|
||||||
float bookH = std::min(500.0f, screenH - 120.0f);
|
float bookH = std::min(520.0f, screenH - 120.0f);
|
||||||
float bookX = screenW - bookW - 10.0f;
|
float bookX = screenW - bookW - 10.0f;
|
||||||
float bookY = 80.0f;
|
float bookY = 80.0f;
|
||||||
|
|
||||||
|
|
@ -88,13 +197,12 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana
|
||||||
|
|
||||||
bool windowOpen = open;
|
bool windowOpen = open;
|
||||||
if (ImGui::Begin("Spellbook", &windowOpen)) {
|
if (ImGui::Begin("Spellbook", &windowOpen)) {
|
||||||
const auto& spells = gameHandler.getKnownSpells();
|
|
||||||
|
|
||||||
if (spells.empty()) {
|
// Tab bar
|
||||||
ImGui::TextDisabled("No spells known.");
|
if (ImGui::BeginTabBar("SpellbookTabs")) {
|
||||||
} else {
|
auto renderTab = [&](const char* label, SpellTab tab, const std::vector<const SpellInfo*>& spellList) {
|
||||||
ImGui::Text("%zu spells known", spells.size());
|
if (ImGui::BeginTabItem(label)) {
|
||||||
ImGui::Separator();
|
currentTab = tab;
|
||||||
|
|
||||||
// Action bar assignment mode indicator
|
// Action bar assignment mode indicator
|
||||||
if (assigningSlot >= 0) {
|
if (assigningSlot >= 0) {
|
||||||
|
|
@ -106,64 +214,124 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spell list
|
if (spellList.empty()) {
|
||||||
|
ImGui::TextDisabled("No spells in this category.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spell list with icons
|
||||||
ImGui::BeginChild("SpellList", ImVec2(0, -60), true);
|
ImGui::BeginChild("SpellList", ImVec2(0, -60), true);
|
||||||
|
|
||||||
for (uint32_t spellId : spells) {
|
float iconSize = 32.0f;
|
||||||
ImGui::PushID(static_cast<int>(spellId));
|
bool isPassiveTab = (tab == SpellTab::PASSIVE);
|
||||||
|
|
||||||
std::string name = getSpellName(spellId);
|
for (const SpellInfo* info : spellList) {
|
||||||
float cd = gameHandler.getSpellCooldown(spellId);
|
ImGui::PushID(static_cast<int>(info->spellId));
|
||||||
|
|
||||||
|
float cd = gameHandler.getSpellCooldown(info->spellId);
|
||||||
bool onCooldown = cd > 0.0f;
|
bool onCooldown = cd > 0.0f;
|
||||||
|
|
||||||
// Color based on state
|
// Dimmer for passive or cooldown spells
|
||||||
if (onCooldown) {
|
if (isPassiveTab || onCooldown) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spell entry - clickable
|
// Icon
|
||||||
char label[256];
|
GLuint iconTex = getSpellIcon(info->iconId, assetManager);
|
||||||
if (onCooldown) {
|
float startY = ImGui::GetCursorPosY();
|
||||||
snprintf(label, sizeof(label), "%s (%.1fs)", name.c_str(), cd);
|
|
||||||
|
if (iconTex) {
|
||||||
|
ImGui::Image((ImTextureID)(uintptr_t)iconTex,
|
||||||
|
ImVec2(iconSize, iconSize));
|
||||||
} else {
|
} else {
|
||||||
snprintf(label, sizeof(label), "%s", name.c_str());
|
// 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui::Selectable(label, false, ImGuiSelectableFlags_AllowDoubleClick)) {
|
ImGui::SameLine();
|
||||||
if (assigningSlot >= 0) {
|
|
||||||
// Assign to action bar slot
|
// 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,
|
gameHandler.setActionBarSlot(assigningSlot,
|
||||||
game::ActionBarSlot::SPELL, spellId);
|
game::ActionBarSlot::SPELL, info->spellId);
|
||||||
assigningSlot = -1;
|
assigningSlot = -1;
|
||||||
} else if (ImGui::IsMouseDoubleClicked(0)) {
|
|
||||||
// Double-click to cast
|
|
||||||
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
||||||
gameHandler.castSpell(spellId, target);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tooltip with spell ID
|
if (ImGui::IsMouseDoubleClicked(0) && !isPassiveTab && !onCooldown) {
|
||||||
if (ImGui::IsItemHovered()) {
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
||||||
|
gameHandler.castSpell(info->spellId, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltip
|
||||||
ImGui::BeginTooltip();
|
ImGui::BeginTooltip();
|
||||||
ImGui::Text("%s", name.c_str());
|
ImGui::Text("%s", info->name.c_str());
|
||||||
ImGui::TextDisabled("Spell ID: %u", spellId);
|
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) {
|
if (!onCooldown) {
|
||||||
ImGui::TextDisabled("Double-click to cast");
|
ImGui::TextDisabled("Double-click to cast");
|
||||||
ImGui::TextDisabled("Use action bar buttons below to assign");
|
}
|
||||||
|
ImGui::TextDisabled("Use buttons below to assign to action bar");
|
||||||
}
|
}
|
||||||
ImGui::EndTooltip();
|
ImGui::EndTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onCooldown) {
|
if (isPassiveTab || onCooldown) {
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Action bar quick-assign buttons
|
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());
|
||||||
|
|
||||||
|
renderTab(generalLabel, SpellTab::GENERAL, generalSpells);
|
||||||
|
renderTab(activeLabel, SpellTab::ACTIVE, activeSpells);
|
||||||
|
renderTab(passiveLabel, SpellTab::PASSIVE, passiveSpells);
|
||||||
|
|
||||||
|
ImGui::EndTabBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action bar quick-assign buttons (not for passive tab)
|
||||||
|
if (currentTab != SpellTab::PASSIVE) {
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Text("Assign to:");
|
ImGui::Text("Assign to:");
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
|
||||||
67
src/ui/talent_screen.cpp
Normal file
67
src/ui/talent_screen.cpp
Normal file
|
|
@ -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<float>(window->getWidth()) : 1280.0f;
|
||||||
|
float screenH = window ? static_cast<float>(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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue