Skip to content

[Bug]: Downranking spell coefficient penalty unmodeled (TBC level penalty formula) #47

Description

@Xerrion

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

  1. New module Engine/LevelPenalty.lua exporting CalculateLevelPenalty(spellLevel, maxLevel, playerLevel) matching cMaNGOS-TBC exactly.
  2. 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).
  3. In Engine/ModifierCalc.lua::BuildModifiedResult, multiply the SP bonus (not base damage) by the level penalty before adding to base.
  4. New test file tests/test_levelpenalty.lua covering: top-rank exemption, sub-20 stacking, MaxLevel+6 formula, healing spells, channels, DoTs.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions