Paragon: expand IsClass hooks and addon pet talent reset

Broaden OnPlayerIsClass for CLASS_CONTEXT_ABILITY, pet/charm/equip contexts; add PARAA C RESET PET TALENTS handler. Update CLIENT-PATCHES.md for patch-enUS-5/6 and PARAA.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Docker Build
2026-05-10 01:36:40 -04:00
parent 7a92231614
commit abb25f56d1
3 changed files with 224 additions and 31 deletions
+191 -28
View File
@@ -7,9 +7,12 @@
#include "Chat.h"
#include "Config.h"
#include "Creature.h"
#include "CreatureData.h"
#include "GameTime.h"
#include "Log.h"
#include "ObjectGuid.h"
#include "Pet.h"
#include "Player.h"
#include "ScriptMgr.h"
#include "SharedDefines.h"
@@ -44,43 +47,142 @@ public:
if (!player || player->getClass() != CLASS_PARAGON)
return std::nullopt;
// Death Knight rune / runic power ability stack (narrow on purpose).
if (unitClass == CLASS_DEATH_KNIGHT && context == CLASS_CONTEXT_ABILITY)
// ============================================================
// Ability stack -- claim ALL nine vanilla classes.
// ============================================================
// CLASS_CONTEXT_ABILITY is read by every class-specific spell
// gate in core / scripts: DK rune mechanics (Spell.cpp,
// SpellEffects.cpp, spell_dk.cpp, SpellAuraEffects.cpp),
// Warrior Titan's Grip / Bladestorm (Player.cpp 3783, 15432,
// PlayerUpdates.cpp 1547), Paladin Rebuke (Player.cpp 15441),
// Shaman dual-wield bookkeeping (Player.cpp 5028), Hunter pet
// / Hunter's Mark gates (spell_item.cpp 3718), Druid Insect
// Swarm / Wild Growth (SpellAuraEffects.cpp 2153, 2232),
// Priest Spirit of Redemption out-of-bounds check (Unit.cpp
// 14238), Rogue pickpocketing (LootHandler.cpp 86/165/385,
// Vehicle.cpp 80). Paragon learns abilities from every class
// through Character Advancement, so claiming all of them lets
// every gated spell script execute its class-specific branch
// for our players. The only downside is double-pathed scripts
// (e.g. a spell with both warrior and rogue branches) will
// pick whichever the script tests first -- acceptable.
if (context == CLASS_CONTEXT_ABILITY)
return true;
// Warrior ability stack: enables warrior-spec ability gates anywhere
// they're checked. None of the currently-traced sites in core/scripts
// gate on (CLASS_WARRIOR, CLASS_CONTEXT_ABILITY), so this is a safe
// forward-compatible claim. Rage generation itself is gated on
// HasActivePowerType(POWER_RAGE) and is wired below.
if (unitClass == CLASS_WARRIOR && context == CLASS_CONTEXT_ABILITY)
return true;
// Reactive melee states: Overpower-on-dodge (warrior), Counterattack window (hunter).
// We intentionally do NOT claim CLASS_ROGUE here: that context skips the generic
// AURA_STATE_DEFENSE update on dodge (Riposte path) in Unit::ProcDamageAndSpellFor.
// ============================================================
// Reactive melee states.
// ============================================================
// Warrior dodge -> AURA_STATE_DEFENSE (Overpower window).
// Hunter parry -> AURA_STATE_HUNTER_PARRY (Counterattack).
// We intentionally do NOT claim CLASS_ROGUE here:
// Unit::ProcDamageAndSpellFor (Unit.cpp 12824) skips the
// generic AURA_STATE_DEFENSE update on dodge for rogues so
// Riposte can take over. Claiming rogue would silently kill
// Overpower for Paragon, and Riposte already works for us via
// the warrior-style state we already grant.
if (context == CLASS_CONTEXT_ABILITY_REACTIVE)
{
if (unitClass == CLASS_WARRIOR || unitClass == CLASS_HUNTER)
return true;
}
// Unified relic / ranged slot for class 12.
// ----------------------------------------------------------------
// CLASS_CONTEXT_EQUIP_RELIC is read in exactly two places in core
// (PlayerStorage.cpp): FindEquipSlot's INVTYPE_RELIC switch, which
// routes Librams/Idols/Totems/Misc/Sigils into EQUIPMENT_SLOT_RANGED
// for the matching class only, and CanEquipUniqueItem's per-subclass
// proficiency gate. By claiming this context for paladin/druid/
// shaman/warlock/dk we let Paragon drop any of those relics into the
// ranged slot exactly the same way each native class does, with no
// core patch and no other side effects (the constant is not read
// anywhere else in the codebase).
// ============================================================
// Pet ownership contexts.
// ============================================================
// CLASS_CONTEXT_PET is read by Pet::AddToWorld, Pet::CreateBase
// AtCreatureInfo, Pet::InitStatsForLevel (twice -- the
// MAX_PET_TYPE bootstrap branch and the per-class attack-time
// scaling), Pet::IsPermanentPetFor, Player::SummonPet,
// Player::CanResummonPet, Spell::EffectTameCreature,
// SpellEffects.cpp (CreateTamedPet debug effects, Eyes of the
// Beast), spell_generic.cpp 1760 (charm-as-pet conversion),
// and PlayerGossip.cpp's hunter stable check.
//
// Bows/guns/crossbows already equip via the regular
// INVTYPE_RANGED/RANGEDRIGHT routing -- weapon proficiencies for
// class 12 are seeded by the Paragon proficiency SQL migrations, so
// they pass the GetSkillValue check in CanEquipUniqueItem.
// The cleanest disambiguation is by the *active pet's* shape:
// HUNTER_PET -> hunter (beast tame)
// SUMMON_PET + DEMON type -> warlock (Imp/VW/Succ/...)
// SUMMON_PET + UNDEAD type -> DK ghoul / Army of Dead
// SUMMON_PET + ELEMENTAL type -> mage water / shaman fire
// For HUNTER specifically the no-pet case is also claimed so
// Tame Beast's EffectTameCreature gate passes during cast.
if (context == CLASS_CONTEXT_PET)
{
Pet const* activePet = const_cast<Player*>(player)->GetPet();
// Hunter beast: claim during taming OR when a HUNTER_PET is
// already active. This is what makes Tame Beast / Call Pet
// / pet stable / Counterattack pet aura feedback work.
if (unitClass == CLASS_HUNTER)
{
if (!activePet || activePet->getPetType() == HUNTER_PET)
return true;
return std::nullopt;
}
// All other classes only claim when an active SUMMON_PET is
// present. We then disambiguate by the creature's type
// because warlock / DK / mage / shaman all use SUMMON_PET.
if (!activePet || activePet->getPetType() != SUMMON_PET)
return std::nullopt;
CreatureTemplate const* tmpl = activePet->GetCreatureTemplate();
if (!tmpl)
return std::nullopt;
switch (unitClass)
{
case CLASS_WARLOCK:
// Drives Master Demonologist / Demonic Knowledge /
// Demonic Pact propagation, last-pet-spell tracking
// (Pet.cpp 112), and IsPermanentPetFor (Pet.cpp
// 2288) so demon pets persist across logins.
if (tmpl->type == CREATURE_TYPE_DEMON)
return true;
break;
case CLASS_DEATH_KNIGHT:
// Risen Ghoul + Army of the Dead. Player.cpp 14354
// and Pet.cpp 243 / 1046 / 2290 read this; without
// it the ghoul is invisible to the owner mid-load
// and ScriptedAI hooks on the ghoul mis-route.
if (tmpl->type == CREATURE_TYPE_UNDEAD)
return true;
break;
case CLASS_MAGE:
// Glyph-of-Eternal-Water permanent Water Elemental
// (entry 510, 37994). Used by Pet.cpp 1047/2292.
if (tmpl->type == CREATURE_TYPE_ELEMENTAL)
return true;
break;
case CLASS_SHAMAN:
// Fire Elemental / Earth Elemental. The base
// engine spawns these as creatures rather than
// proper Pet instances in most code paths, so the
// claim mostly matters for the Pet.cpp 1045 stat
// bootstrap when one is loaded as a SUMMON_PET.
if (tmpl->type == CREATURE_TYPE_ELEMENTAL)
return true;
break;
default:
break;
}
return std::nullopt;
}
// Warlock pet-charm context (Enslave Demon -- Unit.cpp 14828,
// 14894, 15025). Without this claim, charming a demon as a
// Paragon doesn't get the warlock-flavor charm semantics
// (faction-set-on-charm, action-bar layout, charm-break logic).
if (unitClass == CLASS_WARLOCK && context == CLASS_CONTEXT_PET_CHARM)
return true;
// ============================================================
// Equipment contexts.
// ============================================================
// CLASS_CONTEXT_EQUIP_RELIC: PlayerStorage.cpp 224-240 +
// 2475-2493. Routes Librams/Idols/Totems/Misc/Sigils into
// EQUIPMENT_SLOT_RANGED for the matching class. Claim every
// relic-bearing class so a Paragon can drop any of them into
// the ranged slot.
if (context == CLASS_CONTEXT_EQUIP_RELIC)
{
switch (unitClass)
@@ -96,6 +198,67 @@ public:
}
}
// CLASS_CONTEXT_EQUIP_ARMOR_CLASS: PlayerStorage.cpp 2326,
// 2330, 2503-2523. At level 40 each class auto-learns its
// top armor proficiency. Paragon should pick up plate (via
// paladin/DK), shields (paladin/warrior/shaman), mail
// (hunter/shaman), and leather (rogue) so the level-40 train
// event grants Paragon full proficiency and we don't have to
// hand-curate it through the Paragon proficiency SQL.
if (context == CLASS_CONTEXT_EQUIP_ARMOR_CLASS)
{
switch (unitClass)
{
case CLASS_PALADIN:
case CLASS_WARRIOR:
case CLASS_DEATH_KNIGHT:
case CLASS_HUNTER:
case CLASS_SHAMAN:
case CLASS_DRUID:
case CLASS_ROGUE:
return true;
default:
break;
}
}
// CLASS_CONTEXT_EQUIP_SHIELDS: PlayerStorage.cpp 2467-2469.
// Lets a Paragon equip shields without a paladin/warrior/
// shaman skill gate.
if (context == CLASS_CONTEXT_EQUIP_SHIELDS)
{
switch (unitClass)
{
case CLASS_PALADIN:
case CLASS_WARRIOR:
case CLASS_SHAMAN:
return true;
default:
break;
}
}
// CLASS_CONTEXT_WEAPON_SWAP: PlayerStorage.cpp 1920, 2838 --
// rogue uses cooldown spell 6123 instead of 6119 on weapon
// swap (Quick Draw / Combat Potency interactions). Claim
// rogue so Paragon picks up the same cooldown spell.
if (context == CLASS_CONTEXT_WEAPON_SWAP && unitClass == CLASS_ROGUE)
return true;
// ============================================================
// Contexts we DELIBERATELY DO NOT claim:
// ============================================================
// CLASS_CONTEXT_STATS -- Paragon has its own STR/AGI->AP and
// INT/SPI->SP curves wired in StatSystem.cpp's CLASS_PARAGON
// branch (level*2 + STR + AGI - 20 etc.). Claiming any
// vanilla class here would override our curves with theirs.
//
// CLASS_CONTEXT_INIT, _TELEPORT, _QUEST, _TAXI, _SKILL,
// _GRAVEYARD, _CLASS_TRAINER, _TALENT_POINT_CALC -- all
// used by DK Ebon Hold / druid Moonglade starting-zone
// scripts. Paragon doesn't go through those zones and we
// don't want our players bound to Acherus or trapped in
// the DK starting quest gates.
return std::nullopt;
}