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

@ -2728,6 +2728,65 @@ network::Packet TrainerBuySpellPacket::build(uint64_t trainerGuid, uint32_t spel
return packet;
}
// ============================================================
// Talents
// ============================================================
bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) {
// WotLK 3.3.5a SMSG_TALENTS_INFO format:
// uint8 talentSpec (0 or 1 for dual-spec)
// uint8 unspentPoints
// uint8 talentCount
// for each talent:
// uint32 talentId
// uint8 currentRank (0-5)
data = TalentsInfoData{};
if (packet.getSize() - packet.getReadPos() < 3) {
LOG_ERROR("TalentsInfoParser: packet too short");
return false;
}
data.talentSpec = packet.readUInt8();
data.unspentPoints = packet.readUInt8();
uint8_t talentCount = packet.readUInt8();
LOG_INFO("SMSG_TALENTS_INFO: spec=", (int)data.talentSpec,
" unspentPoints=", (int)data.unspentPoints,
" talentCount=", (int)talentCount);
data.talents.reserve(talentCount);
for (uint8_t i = 0; i < talentCount; ++i) {
if (packet.getSize() - packet.getReadPos() < 5) {
LOG_WARNING("TalentsInfoParser: truncated talent data at index ", (int)i);
break;
}
TalentInfo talent;
talent.talentId = packet.readUInt32();
talent.currentRank = packet.readUInt8();
data.talents.push_back(talent);
LOG_INFO(" Talent: id=", talent.talentId, " rank=", (int)talent.currentRank);
}
return true;
}
network::Packet LearnTalentPacket::build(uint32_t talentId, uint32_t requestedRank) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LEARN_TALENT));
packet.writeUInt32(talentId);
packet.writeUInt32(requestedRank);
return packet;
}
network::Packet TalentWipeConfirmPacket::build(bool accept) {
network::Packet packet(static_cast<uint16_t>(Opcode::MSG_TALENT_WIPE_CONFIRM));
packet.writeUInt32(accept ? 1 : 0);
return packet;
}
// ============================================================
// Death/Respawn
// ============================================================