feat(spell): count set bonuses in GetSpellBonusHealing

Set-bonus spells are applied server-side as auras the client never sees
as buffs or known spells, so the derived healing sum missed them —
Lawbringer (3) is "Increase Healing 31" (9317), Judgement (3) is 57
(18037). Add a pass that counts the equipped members of each set from
the item record's set id (OFF_ITEMSTATS_ITEM_SET), reads ItemSet.dbc's
bonus spells and member thresholds, and runs every met bonus through
the same aura decoder — the rule the item tooltip's set-bonus lines use
at 0x52DFB9.

The Ironclad (aura 199) armor conversion stays on
UNIT_FIELD_RESISTANCES[0]: the server's SpellBaseHealingBonusDone uses
GetArmor(), the full total, even though the talent text says "Armor
from items".

Refs #135
This commit is contained in:
Brues
2026-11-09 11:28:51 -06:00
parent c00eb24a8d
commit d7103de564
2 changed files with 53 additions and 3 deletions
+4 -1
View File
@@ -21617,8 +21617,11 @@ GetSpellBonusHealing() -- your +Healing
> Turtle customs) and exact — e.g. Spiritual Guidance 5/5 with 182 Spirit
> adds `int(182 × 25 / 100) = 45`.
>
> Set bonuses count: a bonus whose piece count your equipped gear meets
> (for example Lawbringer's 3-piece +31 Healing) is included.
>
> Residual: assumes talent ranks supersede (only the learned rank is known)
> and misses set-bonus healing granted via a set-completion spell.
> and ignores a set's profession-skill requirement.
### `GetCritChance()` / `GetRangedCritChance()`
+49 -2
View File
@@ -53,8 +53,12 @@
// +15%/rank SpellMod), so an 18-point gap leaked into healing when that pure
// +damage buff was gained. Reading aura 175 directly (above) sidesteps it.
//
// 3. Set bonuses — the bonus spells of every equipped set whose member
// count is met (ItemSet.dbc, the same rule the tooltip's set-bonus
// lines use), decoded through the same aura walk.
//
// Residual: assumes talent ranks supersede (only the learned rank is known —
// otherwise summed) and misses set-bonus healing granted via a set spell.
// otherwise summed) and ignores a set's profession-skill requirement.
#include "Game.h"
#include "Offsets.h"
@@ -185,11 +189,52 @@ void AddSpellHealingAuras(long &total, int spellID, int32_t spirit,
}
}
// Set bonuses the equipped gear has earned. The engine's own set-bonus lines
// (the item tooltip, 0x52DFB9..) count how many of the set's members the
// player has equipped and light a bonus whose member count (ItemSet.dbc
// `+0x8C`) is met; the server applies the same bonus spells as auras. Those
// auras are neither buffs nor known spells, so this is the only pass that
// sees them — Lawbringer (3) is "Increase Healing 31", Judgement (3) is 57.
// A set member's record names its set (`OFF_ITEMSTATS_ITEM_SET`), so the
// equipped count per set is the equipped members' count.
constexpr int kEquipSlots = 19;
void AddSetBonusHealing(long &total, int32_t spirit, int32_t armor) {
struct Worn { uint32_t setID; int count; };
Worn worn[kEquipSlots];
int sets = 0;
for (int slot = 1; slot <= kEquipSlots; ++slot) {
const int itemID = Item::ID::FromCGItem(Item::Location::ResolveEquipmentSlot(slot));
const uint8_t *record = itemID > 0 ? Item::PeekRecord(static_cast<uint32_t>(itemID)) : nullptr;
const uint32_t setID = record ? Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_ITEM_SET) : 0;
if (setID == 0)
continue;
int s = 0;
while (s < sets && worn[s].setID != setID)
++s;
if (s == sets)
worn[sets++] = {setID, 0};
++worn[s].count;
}
for (int s = 0; s < sets; ++s) {
const uint8_t *set = DBC::Record(Offsets::VAR_ITEMSET_RECORDS, Offsets::VAR_ITEMSET_COUNT,
worn[s].setID);
if (set == nullptr)
continue;
auto spells = Game::Ptr<const uint32_t>(set, Offsets::OFF_ITEMSET_SPELL_IDS);
auto needed = Game::Ptr<const uint32_t>(set, Offsets::OFF_ITEMSET_THRESHOLDS);
for (int i = 0; i < Offsets::ITEMSET_MAX_BONUSES; ++i)
if (spells[i] != 0 && needed[i] <= static_cast<uint32_t>(worn[s].count))
AddSpellHealingAuras(total, static_cast<int>(spells[i]), spirit, armor,
/*requirePassive=*/false);
}
}
long ComputeHealing() {
long total = 0;
// Items — flat +healing via the shared StatAccum engine.
for (int slot = 1; slot <= 19; ++slot)
for (int slot = 1; slot <= kEquipSlots; ++slot)
total += ItemFlatHealing(Item::Location::ResolveEquipmentSlot(slot));
// Player Spirit / Armor for the aura-175 / aura-199 conversions.
@@ -199,6 +244,8 @@ long ComputeHealing() {
const int32_t armor = desc ? Game::Read<int32_t>(
desc, Offsets::OFF_UNIT_FIELD_RESISTANCE_ARMOR) : 0;
AddSetBonusHealing(total, spirit, armor);
// Active buffs — flat healing + Spirit/Armor conversions, one pass.
auto *table = reinterpret_cast<const uint8_t *>(
static_cast<uintptr_t>(Offsets::VAR_PLAYER_BUFF_TABLE));