feat: add spell power and healing bonus to WotLK character stats

Tracks PLAYER_FIELD_MOD_DAMAGE_DONE_POS (7 schools at field 1171) and
PLAYER_FIELD_MOD_HEALING_DONE_POS (field 1192) from server update fields.

getSpellPower() returns the max damage bonus across magic schools 1-6.
getHealingPower() returns the raw healing bonus.

Both values displayed in the character screen Combat section alongside
the previously added attack power, dodge, parry, crit, and rating fields.
This commit is contained in:
Kelsi 2026-03-13 08:37:55 -07:00
parent c0ffca68f2
commit 2b79f9d121
6 changed files with 45 additions and 4 deletions

View file

@ -40,6 +40,8 @@
"PLAYER_SKILL_INFO_START": 636,
"PLAYER_EXPLORED_ZONES_START": 1041,
"PLAYER_CHOSEN_TITLE": 1349,
"PLAYER_FIELD_MOD_DAMAGE_DONE_POS": 1171,
"PLAYER_FIELD_MOD_HEALING_DONE_POS": 1192,
"PLAYER_BLOCK_PERCENTAGE": 1024,
"PLAYER_DODGE_PERCENTAGE": 1025,
"PLAYER_PARRY_PERCENTAGE": 1026,

View file

@ -314,6 +314,18 @@ public:
int32_t getMeleeAttackPower() const { return playerMeleeAP_; }
int32_t getRangedAttackPower() const { return playerRangedAP_; }
// Server-authoritative spell damage / healing bonus (WotLK: PLAYER_FIELD_MOD_*).
// getSpellPower returns the max damage bonus across magic schools 1-6 (Holy/Fire/Nature/Frost/Shadow/Arcane).
// Returns -1 if not yet received.
int32_t getSpellPower() const {
int32_t sp = -1;
for (int i = 1; i <= 6; ++i) {
if (playerSpellDmgBonus_[i] > sp) sp = playerSpellDmgBonus_[i];
}
return sp;
}
int32_t getHealingPower() const { return playerHealBonus_; }
// Server-authoritative combat chance percentages (WotLK: PLAYER_* float fields).
// Returns -1.0f if not yet received.
float getDodgePct() const { return playerDodgePct_; }
@ -2820,6 +2832,8 @@ private:
// WotLK secondary combat stats (-1 = not yet received)
int32_t playerMeleeAP_ = -1;
int32_t playerRangedAP_ = -1;
int32_t playerSpellDmgBonus_[7] = {-1,-1,-1,-1,-1,-1,-1}; // per school 0-6
int32_t playerHealBonus_ = -1;
float playerDodgePct_ = -1.0f;
float playerParryPct_ = -1.0f;
float playerBlockPct_ = -1.0f;

View file

@ -63,6 +63,10 @@ enum class UF : uint16_t {
PLAYER_EXPLORED_ZONES_START,
PLAYER_CHOSEN_TITLE, // Active title index (-1 = no title)
// Player spell power / healing bonus (WotLK: PRIVATE — int32 per school)
PLAYER_FIELD_MOD_DAMAGE_DONE_POS, // Spell damage bonus (first of 7 schools)
PLAYER_FIELD_MOD_HEALING_DONE_POS, // Healing bonus
// Player combat stats (WotLK: PRIVATE — float values)
PLAYER_BLOCK_PERCENTAGE, // Block chance %
PLAYER_DODGE_PERCENTAGE, // Dodge chance %

View file

@ -8197,6 +8197,8 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
std::fill(std::begin(playerStats_), std::end(playerStats_), -1);
playerMeleeAP_ = -1;
playerRangedAP_ = -1;
std::fill(std::begin(playerSpellDmgBonus_), std::end(playerSpellDmgBonus_), -1);
playerHealBonus_ = -1;
playerDodgePct_ = -1.0f;
playerParryPct_ = -1.0f;
playerBlockPct_ = -1.0f;
@ -10385,6 +10387,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
};
const uint16_t ufMeleeAP = fieldIndex(UF::UNIT_FIELD_ATTACK_POWER);
const uint16_t ufRangedAP = fieldIndex(UF::UNIT_FIELD_RANGED_ATTACK_POWER);
const uint16_t ufSpDmg1 = fieldIndex(UF::PLAYER_FIELD_MOD_DAMAGE_DONE_POS);
const uint16_t ufHealBonus = fieldIndex(UF::PLAYER_FIELD_MOD_HEALING_DONE_POS);
const uint16_t ufBlockPct = fieldIndex(UF::PLAYER_BLOCK_PERCENTAGE);
const uint16_t ufDodgePct = fieldIndex(UF::PLAYER_DODGE_PERCENTAGE);
const uint16_t ufParryPct = fieldIndex(UF::PLAYER_PARRY_PERCENTAGE);
@ -10429,6 +10433,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
else if (ufMeleeAP != 0xFFFF && key == ufMeleeAP) { playerMeleeAP_ = static_cast<int32_t>(val); }
else if (ufRangedAP != 0xFFFF && key == ufRangedAP) { playerRangedAP_ = static_cast<int32_t>(val); }
else if (ufSpDmg1 != 0xFFFF && key >= ufSpDmg1 && key < ufSpDmg1 + 7) {
playerSpellDmgBonus_[key - ufSpDmg1] = static_cast<int32_t>(val);
}
else if (ufHealBonus != 0xFFFF && key == ufHealBonus) { playerHealBonus_ = static_cast<int32_t>(val); }
else if (ufBlockPct != 0xFFFF && key == ufBlockPct) { std::memcpy(&playerBlockPct_, &val, 4); }
else if (ufDodgePct != 0xFFFF && key == ufDodgePct) { std::memcpy(&playerDodgePct_, &val, 4); }
else if (ufParryPct != 0xFFFF && key == ufParryPct) { std::memcpy(&playerParryPct_, &val, 4); }
@ -10799,6 +10807,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
};
const uint16_t ufMeleeAPV = fieldIndex(UF::UNIT_FIELD_ATTACK_POWER);
const uint16_t ufRangedAPV = fieldIndex(UF::UNIT_FIELD_RANGED_ATTACK_POWER);
const uint16_t ufSpDmg1V = fieldIndex(UF::PLAYER_FIELD_MOD_DAMAGE_DONE_POS);
const uint16_t ufHealBonusV= fieldIndex(UF::PLAYER_FIELD_MOD_HEALING_DONE_POS);
const uint16_t ufBlockPctV = fieldIndex(UF::PLAYER_BLOCK_PERCENTAGE);
const uint16_t ufDodgePctV = fieldIndex(UF::PLAYER_DODGE_PERCENTAGE);
const uint16_t ufParryPctV = fieldIndex(UF::PLAYER_PARRY_PERCENTAGE);
@ -10872,6 +10882,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
else if (ufMeleeAPV != 0xFFFF && key == ufMeleeAPV) { playerMeleeAP_ = static_cast<int32_t>(val); }
else if (ufRangedAPV != 0xFFFF && key == ufRangedAPV) { playerRangedAP_ = static_cast<int32_t>(val); }
else if (ufSpDmg1V != 0xFFFF && key >= ufSpDmg1V && key < ufSpDmg1V + 7) {
playerSpellDmgBonus_[key - ufSpDmg1V] = static_cast<int32_t>(val);
}
else if (ufHealBonusV != 0xFFFF && key == ufHealBonusV) { playerHealBonus_ = static_cast<int32_t>(val); }
else if (ufBlockPctV != 0xFFFF && key == ufBlockPctV) { std::memcpy(&playerBlockPct_, &val, 4); }
else if (ufDodgePctV != 0xFFFF && key == ufDodgePctV) { std::memcpy(&playerDodgePct_, &val, 4); }
else if (ufParryPctV != 0xFFFF && key == ufParryPctV) { std::memcpy(&playerParryPct_, &val, 4); }

View file

@ -64,6 +64,8 @@ static const UFNameEntry kUFNames[] = {
{"ITEM_FIELD_MAXDURABILITY", UF::ITEM_FIELD_MAXDURABILITY},
{"PLAYER_REST_STATE_EXPERIENCE", UF::PLAYER_REST_STATE_EXPERIENCE},
{"PLAYER_CHOSEN_TITLE", UF::PLAYER_CHOSEN_TITLE},
{"PLAYER_FIELD_MOD_DAMAGE_DONE_POS", UF::PLAYER_FIELD_MOD_DAMAGE_DONE_POS},
{"PLAYER_FIELD_MOD_HEALING_DONE_POS", UF::PLAYER_FIELD_MOD_HEALING_DONE_POS},
{"PLAYER_BLOCK_PERCENTAGE", UF::PLAYER_BLOCK_PERCENTAGE},
{"PLAYER_DODGE_PERCENTAGE", UF::PLAYER_DODGE_PERCENTAGE},
{"PLAYER_PARRY_PERCENTAGE", UF::PLAYER_PARRY_PERCENTAGE},

View file

@ -1780,9 +1780,11 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
// Server-authoritative combat stats (WotLK update fields — only shown when received)
if (gh) {
int32_t meleeAP = gh->getMeleeAttackPower();
int32_t rangedAP = gh->getRangedAttackPower();
float dodgePct = gh->getDodgePct();
int32_t meleeAP = gh->getMeleeAttackPower();
int32_t rangedAP = gh->getRangedAttackPower();
int32_t spellPow = gh->getSpellPower();
int32_t healPow = gh->getHealingPower();
float dodgePct = gh->getDodgePct();
float parryPct = gh->getParryPct();
float blockPct = gh->getBlockPct();
float critPct = gh->getCritPct();
@ -1795,7 +1797,7 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
int32_t armorPenR = gh->getCombatRating(24);
int32_t resilR = gh->getCombatRating(14); // CR_CRIT_TAKEN_MELEE = Resilience
bool hasAny = (meleeAP >= 0 || dodgePct >= 0.0f || parryPct >= 0.0f ||
bool hasAny = (meleeAP >= 0 || spellPow >= 0 || dodgePct >= 0.0f || parryPct >= 0.0f ||
blockPct >= 0.0f || critPct >= 0.0f || hitRating >= 0);
if (hasAny) {
ImGui::Spacing();
@ -1805,6 +1807,9 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
if (meleeAP >= 0) ImGui::TextColored(cyan, "Attack Power: %d", meleeAP);
if (rangedAP >= 0 && rangedAP != meleeAP)
ImGui::TextColored(cyan, "Ranged Attack Power: %d", rangedAP);
if (spellPow >= 0) ImGui::TextColored(cyan, "Spell Power: %d", spellPow);
if (healPow >= 0 && healPow != spellPow)
ImGui::TextColored(cyan, "Healing Power: %d", healPow);
if (dodgePct >= 0.0f) ImGui::TextColored(cyan, "Dodge: %.2f%%", dodgePct);
if (parryPct >= 0.0f) ImGui::TextColored(cyan, "Parry: %.2f%%", parryPct);
if (blockPct >= 0.0f) ImGui::TextColored(cyan, "Block: %.2f%%", blockPct);