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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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