Problem
PhDamage applies the full configured spell-power coefficient regardless of spell rank vs player level. TBC''s level penalty formula multiplies the SP bonus by a downrank factor for spells whose MaxLevel is below the player''s level. PhDamage''s calculation overstates the SP contribution of low-rank spells - most acutely on Frostbolt R3 at L70 (~58% overstatement), and progressively on all sub-max-rank spells.
Canonical TBC formula
From cmangos/mangos-tbc Unit.cpp:2920-2936 CalculateLevelPenalty:
uint32 spellLevel = spellProto->spellLevel;
uint32 maxSpellLevel = spellProto->maxLevel;
if (spellLevel <= 0 || spellLevel >= maxSpellLevel) return 1.0f;
float LvlPenalty = 0.0f;
if (spellLevel < 20)
LvlPenalty = (20.0f - spellLevel) * 3.75f;
float LvlFactor = (float(maxSpellLevel) + 6.0f) / float(GetLevel());
if (LvlFactor > 1.0f) LvlFactor = 1.0f;
return (100.0f - LvlPenalty) * LvlFactor / 100.0f;
Critical field semantics:
+ 6 term uses MaxLevel (DBC Spell.MaxLevel, the level base points stop scaling, generally nextRankLevel - 1).
< 20 sub-penalty uses SpellLevel (rank-acquired level, DBC Spell.SpellLevel).
- Top-rank exemption:
spellLevel >= maxLevel returns 1.0 outright.
- Sub-20 multiplier stacks multiplicatively with downrank factor.
Note on AzerothCore divergence: AzerothCore (Unit.cpp:3208-3225) and cMaNGOS-WotLK use SpellLevel + 6. For TBC 2.5.4, use MaxLevel + 6 per cMaNGOS-TBC and the original Blizzard 2005 blue post.
Worked examples (player L70)
| Spell |
SpellLevel |
MaxLevel |
LvlFactor |
Sub-20 |
Penalty |
Notes |
| Frostbolt R11 (top) |
68 |
68 |
1.0 (exempt) |
1.0 |
1.000 |
No penalty |
| Frostbolt R3 |
12 |
~19 |
25/70 = 0.357 |
(20-12)x0.0375 = 0.30 -> x0.70 |
0.250 |
75% reduction |
| Greater Heal R1 |
40 |
~45 |
51/70 = 0.729 |
n/a |
0.729 |
27% reduction |
| Shadow Bolt R5 |
30 |
~35 |
41/70 = 0.586 |
n/a |
0.586 |
41% reduction |
Proposed solution
- New module
Engine/LevelPenalty.lua exporting CalculateLevelPenalty(spellLevel, maxLevel, playerLevel) matching cMaNGOS-TBC exactly.
- Plumb a
maxLevel field into per-rank entries in every Data/SpellData_*.lua file. Convention: maxLevel = nextRankLevel - 1 for non-top ranks; maxLevel = spellLevel for top ranks (triggers exemption via spellLevel >= maxLevel).
- In
Engine/ModifierCalc.lua::BuildModifiedResult, multiply the SP bonus (not base damage) by the level penalty before adding to base.
- New test file
tests/test_levelpenalty.lua covering: top-rank exemption, sub-20 stacking, MaxLevel+6 formula, healing spells, channels, DoTs.
- Update
Engine/Pipeline.lua::CalculateAll to iterate per-rank for headline diagnostics (so action-bar UI exercises lower ranks). This also resolves N1 from the audit.
Engine integration
- Apply uniformly to direct damage, DoTs, HoTs, channels, shield absorbs (no exemptions in cMaNGOS-TBC).
- Apply ONLY to the SP bonus, not base damage.
- Order of operations:
final = (base + sp * coeff * levelPenalty) * modMultiplier.
References
Acceptance criteria
Engine/LevelPenalty.lua exists and matches cMaNGOS-TBC formula exactly.
- All
SpellData_*.lua per-rank entries have a maxLevel field.
BuildModifiedResult applies the penalty to SP bonus only.
- Test suite covers top-rank exemption, sub-20 stacking, healing, channels.
Pipeline.CalculateAll iterates per-rank for diagnostics.
- Frostbolt R3 @ L70 yields effective coefficient ~0.2035 (was ~0.814).
busted --verbose passes.
Problem
PhDamage applies the full configured spell-power coefficient regardless of spell rank vs player level. TBC''s level penalty formula multiplies the SP bonus by a downrank factor for spells whose
MaxLevelis below the player''s level. PhDamage''s calculation overstates the SP contribution of low-rank spells - most acutely on Frostbolt R3 at L70 (~58% overstatement), and progressively on all sub-max-rank spells.Canonical TBC formula
From cmangos/mangos-tbc Unit.cpp:2920-2936 CalculateLevelPenalty:
Critical field semantics:
+ 6term uses MaxLevel (DBCSpell.MaxLevel, the level base points stop scaling, generallynextRankLevel - 1).< 20sub-penalty uses SpellLevel (rank-acquired level, DBCSpell.SpellLevel).spellLevel >= maxLevelreturns 1.0 outright.Note on AzerothCore divergence: AzerothCore (Unit.cpp:3208-3225) and cMaNGOS-WotLK use
SpellLevel + 6. For TBC 2.5.4, use MaxLevel + 6 per cMaNGOS-TBC and the original Blizzard 2005 blue post.Worked examples (player L70)
Proposed solution
Engine/LevelPenalty.luaexportingCalculateLevelPenalty(spellLevel, maxLevel, playerLevel)matching cMaNGOS-TBC exactly.maxLevelfield into per-rank entries in everyData/SpellData_*.luafile. Convention:maxLevel = nextRankLevel - 1for non-top ranks;maxLevel = spellLevelfor top ranks (triggers exemption viaspellLevel >= maxLevel).Engine/ModifierCalc.lua::BuildModifiedResult, multiply the SP bonus (not base damage) by the level penalty before adding to base.tests/test_levelpenalty.luacovering: top-rank exemption, sub-20 stacking, MaxLevel+6 formula, healing spells, channels, DoTs.Engine/Pipeline.lua::CalculateAllto iterate per-rank for headline diagnostics (so action-bar UI exercises lower ranks). This also resolves N1 from the audit.Engine integration
final = (base + sp * coeff * levelPenalty) * modMultiplier.References
Acceptance criteria
Engine/LevelPenalty.luaexists and matches cMaNGOS-TBC formula exactly.SpellData_*.luaper-rank entries have amaxLevelfield.BuildModifiedResultapplies the penalty to SP bonus only.Pipeline.CalculateAlliterates per-rank for diagnostics.busted --verbosepasses.