Paragon: Runeforging support (panel-purchasable, no anvil required)

Lets the Paragon class buy Runeforging from the Character Advancement
panel and apply rune enchants from anywhere in the world without needing
to be near a runeforge GameObject. Three carve-outs work together:

* Spell.cpp: bypass the SpellFocusObject GO proximity check when the
  caster is a Paragon and the spell belongs to SKILL_RUNEFORGING (776).
  Stock DK behaviour is unchanged -- the bypass is gated on
  getClass() == CLASS_PARAGON, not on the IsClass() context hook.

* Player.cpp: skip the Paragon class-skill cascade block for skill 776
  so the rune-enchant SLA cascade actually fires. Without this the
  player gets the Runeforging skill but no rune options at the anvil.

* Paragon_Essence.cpp:
  - Treat SKILL_RUNEFORGING children as a meta-skill cluster: cascade
    them like passives even though they're active casts, so they stick
    as panel_spell_children and get cleaned up via the standard refund
    path.
  - Whitelist the 8 basic rune-enchants in PruneSkillLineCascadeChildren
    so they don't get evicted as "active in children = legacy garbage".
  - Force-attach them in PanelLearnSpellChain (the SLA rows ship with
    AcquireMethod=0, so the engine cascade alone won't grant them).
  - Add an OnPlayerLogin fixup so existing Paragons who bought
    Runeforging before this change get the 8 runes retro-granted.
  - Stop filtering SPELL_ATTR0_DO_NOT_DISPLAY in PushSpellSnapshot --
    Runeforging itself is hidden in the DBC but is a real panel
    purchase that must show in the Overview tab.

The two advanced runes (Stoneskin Gargoyle, Nerubian Carapace) are
intentionally excluded from the auto-grant -- retail gates them behind
heroic dungeon / raid item drops and the SLA AcquireMethod=0 honours
that gating.

No SQL migration needed; works against existing DBC + SLA data.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Docker Build
2026-05-12 04:49:38 -04:00
parent b8826370c6
commit da17074a63
3 changed files with 220 additions and 8 deletions
+14 -1
View File
@@ -12048,7 +12048,20 @@ void Player::learnSkillRewardedSpells(uint32 skill_id, uint32 skill_value)
// weapon, language, and racial skill cascades stay enabled so things
// like recipe auto-learn, weapon proficiencies, and racial perks
// still work.
if (getClass() == CLASS_PARAGON)
//
// Carve-out: SKILL_RUNEFORGING (776) is a CLASS-category skill but
// behaves like a profession in this context — the player buys ONE
// panel ability (Runeforging, spell 53428) and the rune-enchant
// spells (Rune of the Fallen Crusader, Razorice, Cinderglacier, ...)
// are supposed to come along for the ride via the standard SLA
// cascade, exactly the same way they do for a stock DK. Without
// this carve-out, the early-return below blocks the cascade and a
// Paragon who buys Runeforging gets the skill but no actual rune
// options at the runeforge anvil. The cascade only fires once per
// skill-grant for 776 (it's not on UpdateSkillsForLevel) so the
// "leaking back into the spellbook" concern that motivates the
// early-return doesn't apply to this skill.
if (getClass() == CLASS_PARAGON && skill_id != SKILL_RUNEFORGING)
{
if (SkillLineEntry const* sl = sSkillLineStore.LookupEntry(skill_id))
if (sl->categoryId == SKILL_CATEGORY_CLASS)
+30
View File
@@ -7860,6 +7860,36 @@ SpellCastResult Spell::CheckSpellFocus()
// check spell focus object
if (m_spellInfo->RequiresSpellFocus)
{
// Fractured / Paragon: skip the GO proximity check for Paragon
// casters when the spell is a runeforge enchant (skill line 776).
// Paragons get a Runeforge tab in the Character Advancement
// panel that lets them apply rune-enchants from anywhere in the
// world -- no need to fly back to Acherus or find the nearest
// Eastern Plaguelands anvil. The skill-line gate keeps the
// bypass tightly scoped: only the 10 SkillLineAbility-tagged
// rune-enchant spells qualify, every other spell that uses
// SpellFocusObject (Enchanting bench, Cooking fire, Lockpicking
// anvil, etc.) keeps its requirement intact.
//
// DK / other class casters are unchanged -- this carve-out
// intentionally checks getClass() == CLASS_PARAGON rather than
// IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_ABILITY) (which
// would also return true for Paragons via mod-paragon's hook).
// Stock DK behavior must stay vanilla; the QoL bypass is a
// class-12 feature only.
if (m_caster && m_caster->IsPlayer()
&& m_caster->ToPlayer()->getClass() == CLASS_PARAGON)
{
auto bounds = sSpellMgr->GetSkillLineAbilityMapBounds(m_spellInfo->Id);
for (auto it = bounds.first; it != bounds.second; ++it)
{
if (it->second->SkillLine == SKILL_RUNEFORGING)
{
return SPELL_CAST_OK;
}
}
}
CellCoord p(Acore::ComputeCellCoord(m_caster->GetPositionX(), m_caster->GetPositionY()));
Cell cell(p);