feat: track and display WotLK server-authoritative combat stats

Adds update field tracking for WotLK secondary combat statistics:
- UNIT_FIELD_ATTACK_POWER / RANGED_ATTACK_POWER (fields 123, 126)
- PLAYER_DODGE/PARRY/BLOCK/CRIT_PERCENTAGE (fields 1025-1029)
- PLAYER_RANGED_CRIT_PERCENTAGE, PLAYER_SPELL_CRIT_PERCENTAGE1 (1030, 1032)
- PLAYER_FIELD_COMBAT_RATING_1 (25 slots at 1231, hit/expertise/haste/etc.)

Both CREATE_OBJECT and VALUES update paths now populate these fields.
The Character screen Stats tab shows them when received from the server,
with graceful fallback when not available (Classic/TBC expansions).

Field indices verified against AzerothCore 3.3.5a UpdateFields.h.
This commit is contained in:
Kelsi 2026-03-13 08:35:18 -07:00
parent ea7b276125
commit c0ffca68f2
7 changed files with 166 additions and 3 deletions

View file

@ -309,6 +309,31 @@ public:
return playerStats_[idx];
}
// Server-authoritative attack power (WotLK: UNIT_FIELD_ATTACK_POWER / RANGED).
// Returns -1 if not yet received.
int32_t getMeleeAttackPower() const { return playerMeleeAP_; }
int32_t getRangedAttackPower() const { return playerRangedAP_; }
// Server-authoritative combat chance percentages (WotLK: PLAYER_* float fields).
// Returns -1.0f if not yet received.
float getDodgePct() const { return playerDodgePct_; }
float getParryPct() const { return playerParryPct_; }
float getBlockPct() const { return playerBlockPct_; }
float getCritPct() const { return playerCritPct_; }
float getRangedCritPct() const { return playerRangedCritPct_; }
// Spell crit by school (0=Physical,1=Holy,2=Fire,3=Nature,4=Frost,5=Shadow,6=Arcane)
float getSpellCritPct(int school = 1) const {
if (school < 0 || school > 6) return -1.0f;
return playerSpellCritPct_[school];
}
// Server-authoritative combat ratings (WotLK: PLAYER_FIELD_COMBAT_RATING_1+idx).
// Returns -1 if not yet received. Indices match AzerothCore CombatRating enum.
int32_t getCombatRating(int cr) const {
if (cr < 0 || cr > 24) return -1;
return playerCombatRatings_[cr];
}
// Inventory
Inventory& getInventory() { return inventory; }
const Inventory& getInventory() const { return inventory; }
@ -2792,6 +2817,16 @@ private:
int32_t playerResistances_[6] = {}; // [0]=Holy,[1]=Fire,[2]=Nature,[3]=Frost,[4]=Shadow,[5]=Arcane
// Server-authoritative primary stats: [0]=STR [1]=AGI [2]=STA [3]=INT [4]=SPI; -1 = not received yet
int32_t playerStats_[5] = {-1, -1, -1, -1, -1};
// WotLK secondary combat stats (-1 = not yet received)
int32_t playerMeleeAP_ = -1;
int32_t playerRangedAP_ = -1;
float playerDodgePct_ = -1.0f;
float playerParryPct_ = -1.0f;
float playerBlockPct_ = -1.0f;
float playerCritPct_ = -1.0f;
float playerRangedCritPct_ = -1.0f;
float playerSpellCritPct_[7] = {-1.0f,-1.0f,-1.0f,-1.0f,-1.0f,-1.0f,-1.0f};
int32_t playerCombatRatings_[25] = {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1};
// Some servers/custom clients shift update field indices. We can auto-detect coinage by correlating
// money-notify deltas with update-field diffs and then overriding UF::PLAYER_FIELD_COINAGE at runtime.
uint32_t pendingMoneyDelta_ = 0;

View file

@ -42,6 +42,10 @@ enum class UF : uint16_t {
UNIT_FIELD_STAT4, // Spirit
UNIT_END,
// Unit combat fields (WotLK: PRIVATE+OWNER — only visible for the player character)
UNIT_FIELD_ATTACK_POWER, // Melee attack power (int32)
UNIT_FIELD_RANGED_ATTACK_POWER, // Ranged attack power (int32)
// Player fields
PLAYER_FLAGS,
PLAYER_BYTES,
@ -59,6 +63,15 @@ enum class UF : uint16_t {
PLAYER_EXPLORED_ZONES_START,
PLAYER_CHOSEN_TITLE, // Active title index (-1 = no title)
// Player combat stats (WotLK: PRIVATE — float values)
PLAYER_BLOCK_PERCENTAGE, // Block chance %
PLAYER_DODGE_PERCENTAGE, // Dodge chance %
PLAYER_PARRY_PERCENTAGE, // Parry chance %
PLAYER_CRIT_PERCENTAGE, // Melee crit chance %
PLAYER_RANGED_CRIT_PERCENTAGE, // Ranged crit chance %
PLAYER_SPELL_CRIT_PERCENTAGE1, // Spell crit chance % (first school; 7 consecutive float fields)
PLAYER_FIELD_COMBAT_RATING_1, // First of 25 int32 combat rating slots (CR_* indices)
// GameObject fields
GAMEOBJECT_DISPLAYID,

View file

@ -149,7 +149,8 @@ private:
void renderEquipmentPanel(game::Inventory& inventory);
void renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections = false);
void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor = 0,
const int32_t* serverStats = nullptr, const int32_t* serverResists = nullptr);
const int32_t* serverStats = nullptr, const int32_t* serverResists = nullptr,
const game::GameHandler* gh = nullptr);
void renderReputationPanel(game::GameHandler& gameHandler);
void renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,