Implement complete talent system with dual spec support

Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks

DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))

UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation

Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
This commit is contained in:
Kelsi 2026-02-10 02:00:13 -08:00
parent bf03044a63
commit e7556605d7
8 changed files with 860 additions and 29 deletions

View file

@ -78,6 +78,26 @@ using WorldConnectFailureCallback = std::function<void(const std::string& reason
*/
class GameHandler {
public:
// Talent data structures (must be public for use in templates)
struct TalentEntry {
uint32_t talentId = 0;
uint32_t tabId = 0; // Which talent tree
uint8_t row = 0; // Tier (0-10)
uint8_t column = 0; // Column (0-3)
uint32_t rankSpells[5] = {}; // Spell IDs for ranks 1-5
uint32_t prereqTalent[3] = {}; // Required talents
uint8_t prereqRank[3] = {}; // Required ranks
uint8_t maxRank = 0; // Number of ranks (1-5)
};
struct TalentTabEntry {
uint32_t tabId = 0;
std::string name;
uint32_t classMask = 0; // Which classes can use this tab
uint8_t orderIndex = 0; // Display order (0-2)
std::string backgroundFile; // Texture path
};
GameHandler();
~GameHandler();
@ -327,6 +347,35 @@ public:
float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; }
float getCastTimeRemaining() const { return castTimeRemaining; }
// Talents
uint8_t getActiveTalentSpec() const { return activeTalentSpec_; }
uint8_t getUnspentTalentPoints() const { return unspentTalentPoints_[activeTalentSpec_]; }
uint8_t getUnspentTalentPoints(uint8_t spec) const { return spec < 2 ? unspentTalentPoints_[spec] : 0; }
const std::unordered_map<uint32_t, uint8_t>& getLearnedTalents() const { return learnedTalents_[activeTalentSpec_]; }
const std::unordered_map<uint32_t, uint8_t>& getLearnedTalents(uint8_t spec) const {
static std::unordered_map<uint32_t, uint8_t> empty;
return spec < 2 ? learnedTalents_[spec] : empty;
}
uint8_t getTalentRank(uint32_t talentId) const {
auto it = learnedTalents_[activeTalentSpec_].find(talentId);
return (it != learnedTalents_[activeTalentSpec_].end()) ? it->second : 0;
}
void learnTalent(uint32_t talentId, uint32_t requestedRank);
void switchTalentSpec(uint8_t newSpec);
// Talent DBC access
const TalentEntry* getTalentEntry(uint32_t talentId) const {
auto it = talentCache_.find(talentId);
return (it != talentCache_.end()) ? &it->second : nullptr;
}
const TalentTabEntry* getTalentTabEntry(uint32_t tabId) const {
auto it = talentTabCache_.find(tabId);
return (it != talentTabCache_.end()) ? &it->second : nullptr;
}
const std::unordered_map<uint32_t, TalentEntry>& getAllTalents() const { return talentCache_; }
const std::unordered_map<uint32_t, TalentTabEntry>& getAllTalentTabs() const { return talentTabCache_; }
void loadTalentDbc();
// Action bar
static constexpr int ACTION_BAR_SLOTS = 12;
std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() { return actionBar; }
@ -436,6 +485,10 @@ public:
// Player GUID
uint64_t getPlayerGuid() const { return playerGuid; }
uint8_t getPlayerClass() const {
const Character* ch = getActiveCharacter();
return ch ? static_cast<uint8_t>(ch->characterClass) : 0;
}
void setPlayerGuid(uint64_t guid) { playerGuid = guid; }
// Player death state
@ -703,6 +756,9 @@ private:
void handleRemovedSpell(network::Packet& packet);
void handleUnlearnSpells(network::Packet& packet);
// ---- Talent handlers ----
void handleTalentsInfo(network::Packet& packet);
// ---- Phase 4 handlers ----
void handleGroupInvite(network::Packet& packet);
void handleGroupDecline(network::Packet& packet);
@ -918,6 +974,14 @@ private:
bool casting = false;
uint32_t currentCastSpellId = 0;
float castTimeRemaining = 0.0f;
// Talents (dual-spec support)
uint8_t activeTalentSpec_ = 0; // Currently active spec (0 or 1)
uint8_t unspentTalentPoints_[2] = {0, 0}; // Unspent points per spec
std::unordered_map<uint32_t, uint8_t> learnedTalents_[2]; // Learned talents per spec
std::unordered_map<uint32_t, TalentEntry> talentCache_; // talentId -> entry
std::unordered_map<uint32_t, TalentTabEntry> talentTabCache_; // tabId -> entry
bool talentDbcLoaded_ = false;
float castTimeTotal = 0.0f;
std::array<ActionBarSlot, 12> actionBar{};
std::vector<AuraSlot> playerAuras;

View file

@ -168,6 +168,11 @@ enum class Opcode : uint16_t {
SMSG_SET_FLAT_SPELL_MODIFIER = 0x266,
SMSG_SET_PCT_SPELL_MODIFIER = 0x267,
// ---- Talents ----
SMSG_TALENTS_INFO = 0x4C0,
CMSG_LEARN_TALENT = 0x251,
MSG_TALENT_WIPE_CONFIRM = 0x2AB,
// ---- Phase 4: Group/Party ----
CMSG_GROUP_INVITE = 0x06E,
SMSG_GROUP_INVITE = 0x06F,

View file

@ -1753,6 +1753,43 @@ public:
static network::Packet build(uint64_t trainerGuid, uint32_t spellId);
};
// ============================================================
// Talents
// ============================================================
/** Talent info for a single talent */
struct TalentInfo {
uint32_t talentId = 0; // Talent.dbc ID
uint8_t currentRank = 0; // 0-5 (0 = not learned)
};
/** SMSG_TALENTS_INFO data */
struct TalentsInfoData {
uint8_t talentSpec = 0; // Active spec (0 or 1 for dual-spec)
uint8_t unspentPoints = 0; // Talent points available
std::vector<TalentInfo> talents; // Learned talents
bool isValid() const { return true; }
};
/** SMSG_TALENTS_INFO parser */
class TalentsInfoParser {
public:
static bool parse(network::Packet& packet, TalentsInfoData& data);
};
/** CMSG_LEARN_TALENT packet builder */
class LearnTalentPacket {
public:
static network::Packet build(uint32_t talentId, uint32_t requestedRank);
};
/** MSG_TALENT_WIPE_CONFIRM packet builder */
class TalentWipeConfirmPacket {
public:
static network::Packet build(bool accept);
};
// ============================================================
// Taxi / Flight Paths
// ============================================================

View file

@ -145,6 +145,7 @@ private:
// ---- New UI renders ----
void renderActionBar(game::GameHandler& gameHandler);
void renderBagBar(game::GameHandler& gameHandler);
void renderXpBar(game::GameHandler& gameHandler);
void renderCastBar(game::GameHandler& gameHandler);
void renderCombatText(game::GameHandler& gameHandler);
@ -195,6 +196,10 @@ private:
int actionBarDragSlot_ = -1;
GLuint actionBarDragIcon_ = 0;
// Bag bar textures
GLuint backpackIconTexture_ = 0;
GLuint emptyBagSlotTexture_ = 0;
static std::string getSettingsPath();
// Gender placeholder replacement

View file

@ -2,8 +2,13 @@
#include "game/game_handler.hpp"
#include <imgui.h>
#include <GL/glew.h>
#include <unordered_map>
#include <string>
namespace wowee {
namespace pipeline { class AssetManager; }
namespace ui {
class TalentScreen {
@ -14,8 +19,24 @@ public:
void setOpen(bool o) { open = o; }
private:
void renderTalentTrees(game::GameHandler& gameHandler);
void renderTalentTree(game::GameHandler& gameHandler, uint32_t tabId);
void renderTalent(game::GameHandler& gameHandler, const game::GameHandler::TalentEntry& talent);
void loadSpellDBC(pipeline::AssetManager* assetManager);
void loadSpellIconDBC(pipeline::AssetManager* assetManager);
GLuint getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager);
bool open = false;
bool nKeyWasDown = false;
// DBC caches
bool spellDbcLoaded = false;
bool iconDbcLoaded = false;
std::unordered_map<uint32_t, uint32_t> spellIconIds; // spellId -> iconId
std::unordered_map<uint32_t, std::string> spellIconPaths; // iconId -> path
std::unordered_map<uint32_t, GLuint> spellIconCache; // iconId -> texture
std::unordered_map<uint32_t, std::string> spellTooltips; // spellId -> description
};
} // namespace ui