abb25f56d1
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>
522 lines
22 KiB
C++
522 lines
22 KiB
C++
/*
|
||
* mod-paragon — Paragon (class 12) class hooks
|
||
*
|
||
* See README for design. This file wires Player::IsClass / HasActivePowerType
|
||
* so Paragon can reuse other classes' mechanics in narrowly scoped contexts.
|
||
*/
|
||
|
||
#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"
|
||
#include "SpellScript.h"
|
||
#include "SpellScriptLoader.h"
|
||
#include "WorldPacket.h"
|
||
#include "WorldSession.h"
|
||
|
||
#include <fmt/format.h>
|
||
#include <string>
|
||
#include <unordered_map>
|
||
|
||
class Paragon_PlayerScript : public PlayerScript
|
||
{
|
||
public:
|
||
Paragon_PlayerScript() : PlayerScript("Paragon_PlayerScript", {
|
||
PLAYERHOOK_ON_PLAYER_IS_CLASS,
|
||
PLAYERHOOK_ON_PLAYER_HAS_ACTIVE_POWER_TYPE,
|
||
PLAYERHOOK_ON_UPDATE,
|
||
PLAYERHOOK_ON_LOGIN,
|
||
PLAYERHOOK_ON_LOGOUT,
|
||
PLAYERHOOK_ON_AFTER_UPDATE_MAX_POWER
|
||
})
|
||
{
|
||
LOG_INFO("module", "[paragon] Paragon_PlayerScript registered "
|
||
"(MultiResource.HasActivePowers={})",
|
||
sConfigMgr->GetOption<bool>("Paragon.MultiResource.HasActivePowers", true));
|
||
}
|
||
|
||
[[nodiscard]] Optional<bool> OnPlayerIsClass(Player const* player, Classes unitClass, ClassContext context) override
|
||
{
|
||
if (!player || player->getClass() != CLASS_PARAGON)
|
||
return std::nullopt;
|
||
|
||
// ============================================================
|
||
// 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;
|
||
|
||
// ============================================================
|
||
// 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;
|
||
}
|
||
|
||
// ============================================================
|
||
// 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.
|
||
//
|
||
// 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)
|
||
{
|
||
case CLASS_PALADIN:
|
||
case CLASS_DRUID:
|
||
case CLASS_SHAMAN:
|
||
case CLASS_WARLOCK:
|
||
case CLASS_DEATH_KNIGHT:
|
||
return true;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
[[nodiscard]] bool OnPlayerHasActivePowerType(Player const* player, Powers power) override
|
||
{
|
||
if (!player || player->getClass() != CLASS_PARAGON)
|
||
return false;
|
||
|
||
if (power == POWER_RUNIC_POWER || power == POWER_RUNE)
|
||
return true;
|
||
|
||
if (sConfigMgr->GetOption<bool>("Paragon.MultiResource.HasActivePowers", true))
|
||
{
|
||
switch (power)
|
||
{
|
||
case POWER_MANA:
|
||
case POWER_RAGE:
|
||
case POWER_ENERGY:
|
||
case POWER_FOCUS:
|
||
return true;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// ChrClasses.dbc says POWER_RUNE has no pool (Unit::GetCreatePowers returns 0
|
||
// for POWER_RUNE), so Player::InitStatsForLevel and the level-reset path
|
||
// both clobber MaxPower(POWER_RUNE)=0 every login. The 3.3.5 client greys
|
||
// the action button for any rune-cost spell when UnitPowerMax(player,
|
||
// SPELL_POWER_RUNES) is 0 — visual rune pips can still animate via
|
||
// PLAYER_RUNE_REGEN_*, but the spell stays unusable.
|
||
//
|
||
// Fix: pin POWER_RUNE max=8 (and current=8 in the easy spots) for Paragon
|
||
// anywhere UpdateMaxPower runs. Rune availability is still authoritatively
|
||
// tracked in m_runes; this just keeps the client engine happy.
|
||
void OnPlayerAfterUpdateMaxPower(Player* player, Powers& power, float& value) override
|
||
{
|
||
if (!player || player->getClass() != CLASS_PARAGON)
|
||
return;
|
||
|
||
if (power == POWER_RUNE)
|
||
value = 8.0f;
|
||
else if (power == POWER_RUNIC_POWER)
|
||
value = 1000.0f;
|
||
}
|
||
|
||
// Login: re-claim MaxPower for POWER_RUNE / POWER_RUNIC_POWER and seed
|
||
// the rune state to the client. Player::LoadFromDB runs InitStatsForLevel
|
||
// which calls Unit::SetMaxPower with the value from GetCreatePowers — and
|
||
// GetCreatePowers returns 0 for POWER_RUNE (engine assumption: only DK has
|
||
// runes; class is identified via getClass(), not IsClass()). So we have to
|
||
// overwrite MaxPower here once the player is fully loaded. ResyncRunes is
|
||
// a one-shot push so the rune frame is correct on first appearance.
|
||
void OnPlayerLogin(Player* player) override
|
||
{
|
||
if (!player || player->getClass() != CLASS_PARAGON)
|
||
return;
|
||
|
||
player->SetMaxPower(POWER_RUNE, 8);
|
||
if (player->GetMaxPower(POWER_RUNIC_POWER) <= 0)
|
||
player->SetMaxPower(POWER_RUNIC_POWER, 1000);
|
||
player->ResyncRunes(MAX_RUNES);
|
||
|
||
ParagonRuneSyncState seed;
|
||
seed.lastReadyMask = 0;
|
||
for (uint8 i = 0; i < MAX_RUNES; ++i)
|
||
if (player->GetRuneCooldown(i) == 0)
|
||
seed.lastReadyMask |= (1u << i);
|
||
runeSyncByGuid[player->GetGUID()] = seed;
|
||
}
|
||
|
||
void OnPlayerLogout(Player* player) override
|
||
{
|
||
if (!player)
|
||
return;
|
||
runeSyncByGuid.erase(player->GetGUID());
|
||
}
|
||
|
||
// Per-tick rune state pump for Paragon.
|
||
//
|
||
// Two responsibilities:
|
||
//
|
||
// 1. UNIT_FIELD_POWER5 (POWER_RUNE) tracks the live ready-rune count. The
|
||
// 3.3.5 client's local "is this action usable" check on rune-cost
|
||
// spells reads UnitPower(player, SPELL_POWER_RUNES); for DK that's
|
||
// engine-managed but Paragon must own it.
|
||
//
|
||
// 2. Push SMSG_RESYNC_RUNES *only* on ready-bitmask transitions (rune
|
||
// consumed → on cd, or finished cd → ready). We deliberately do NOT
|
||
// push during the cooldown — Player::ResyncRunes encodes the per-rune
|
||
// "passed cooldown" byte as `255 - cd_ms * 51`, which is monotonic
|
||
// only for cd values in seconds (0..5). With cd in milliseconds the
|
||
// byte oscillates wildly tick-to-tick (cd=10000 → 207, cd=9000 → 7,
|
||
// cd=8000 → 63). Frequent resyncs feed the client garbage and freeze
|
||
// the rune frame. One mask-change resync is enough; sweeps run on the
|
||
// client's own 10s timer started from SMSG_SPELL_GO's CAST_FLAG_RUNE_LIST
|
||
// payload (which uses correct encoding) plus a Lua-side
|
||
// GetRuneCooldown poll in RuneFrame.lua.
|
||
void OnPlayerUpdate(Player* player, uint32 /*p_time*/) override
|
||
{
|
||
if (!player || player->getClass() != CLASS_PARAGON)
|
||
return;
|
||
|
||
uint8 readyMask = 0;
|
||
uint8 readyCount = 0;
|
||
for (uint8 i = 0; i < MAX_RUNES; ++i)
|
||
{
|
||
if (player->GetRuneCooldown(i) == 0)
|
||
{
|
||
readyMask |= (1u << i);
|
||
++readyCount;
|
||
}
|
||
}
|
||
|
||
if (uint32(readyCount) != player->GetPower(POWER_RUNE))
|
||
player->SetPower(POWER_RUNE, readyCount);
|
||
|
||
ParagonRuneSyncState& st = runeSyncByGuid[player->GetGUID()];
|
||
if (readyMask != st.lastReadyMask)
|
||
{
|
||
player->ResyncRunes(MAX_RUNES);
|
||
st.lastReadyMask = readyMask;
|
||
|
||
// Authoritative rune CD pump (PARAA "R RUNES cd0 cd1 ... cd5",
|
||
// ms remaining per slot, 0 = ready). The 3.3.5 client engine
|
||
// class-gates SMSG_RESYNC_RUNES / SMSG_SPELL_GO RUNE_LIST to DK,
|
||
// so the Paragon RuneFrame sim drives the visual entirely off
|
||
// COMBAT_LOG_EVENT_UNFILTERED:SPELL_CAST_SUCCESS. The combat log
|
||
// arrives ~100–200ms after the server already started the
|
||
// cooldown, so the client's local timer trails the server. When
|
||
// the user spams a rune spell, the server's slot refreshes
|
||
// first, accepts the next cast, but the client UI still shows
|
||
// CD remaining → "leak-through" past a greyed icon. Pushing the
|
||
// actual remaining ms on every mask transition keeps the
|
||
// visual locked to server state.
|
||
std::string body = "R RUNES";
|
||
for (uint8 i = 0; i < MAX_RUNES; ++i)
|
||
body += " " + std::to_string(player->GetRuneCooldown(i));
|
||
std::string const payload = std::string(kParagonAddonPrefix) + "\t" + body;
|
||
WorldPacket runePkt;
|
||
ChatHandler::BuildChatPacket(runePkt, CHAT_MSG_WHISPER, LANG_ADDON, player, player, payload);
|
||
player->SendDirectMessage(&runePkt);
|
||
}
|
||
|
||
// Combo point pump: the 3.3.5 client engine class-gates SMSG_UPDATE_COMBO_POINTS
|
||
// to rogue / druid, so the Paragon UI sim never sees CP changes from
|
||
// Honor Among Thieves / Mutilate / etc. via either the engine state or
|
||
// the client-side combat-log inference (HAT's 51699 trigger fires with a
|
||
// null target and doesn't always emit SPELL_CAST_SUCCESS in the log).
|
||
// Push the count over PARAA whenever it changes; the addon's combo
|
||
// simulator listens for "R CP <n>" and overwrites paragonCP, so the
|
||
// ComboFrame on the target frame paints reliably.
|
||
int8 const cp = player->GetComboPoints();
|
||
if (cp != st.lastCp)
|
||
{
|
||
std::string const payload = std::string(kParagonAddonPrefix) + "\t"
|
||
+ fmt::format("R CP {}", int32(cp));
|
||
WorldPacket data;
|
||
ChatHandler::BuildChatPacket(data, CHAT_MSG_WHISPER, LANG_ADDON, player, player, payload);
|
||
player->SendDirectMessage(&data);
|
||
st.lastCp = cp;
|
||
}
|
||
|
||
if (!sConfigMgr->GetOption<bool>("Paragon.Diag.RuneTrace", false))
|
||
return;
|
||
|
||
static thread_local time_t lastLogged = 0;
|
||
time_t const wall = GameTime::GetGameTime().count();
|
||
if (wall - lastLogged < 5)
|
||
return;
|
||
lastLogged = wall;
|
||
|
||
std::string out;
|
||
for (uint8 i = 0; i < MAX_RUNES; ++i)
|
||
{
|
||
char buf[64];
|
||
snprintf(buf, sizeof(buf), "[%u: type=%u cur=%u cd=%u]",
|
||
i,
|
||
uint32(player->GetBaseRune(i)),
|
||
uint32(player->GetCurrentRune(i)),
|
||
uint32(player->GetRuneCooldown(i)));
|
||
out += buf;
|
||
}
|
||
LOG_INFO("module", "[paragon-diag] {} runes: {} mask=0x{:02x}",
|
||
player->GetName(), out, uint32(readyMask));
|
||
}
|
||
|
||
private:
|
||
struct ParagonRuneSyncState
|
||
{
|
||
uint8 lastReadyMask{0xFFu}; // sentinel: no prior snapshot
|
||
int8 lastCp{-1}; // sentinel: no prior snapshot
|
||
};
|
||
|
||
static constexpr char const* kParagonAddonPrefix = "PARAA";
|
||
|
||
static std::unordered_map<ObjectGuid, ParagonRuneSyncState> runeSyncByGuid;
|
||
};
|
||
|
||
std::unordered_map<ObjectGuid, Paragon_PlayerScript::ParagonRuneSyncState> Paragon_PlayerScript::runeSyncByGuid;
|
||
|
||
// Arcane Torrent (28730) for Paragon: Blood Elf racial skill line 756 has
|
||
// three Arcane Torrent variants in stock WotLK (28730 mana, 25046 rogue
|
||
// energy, 50613 DK runic power). For Paragon Blood Elves we keep only 28730
|
||
// (see migration 2026_05_10_03.sql) and turn it into a "combined" version:
|
||
// the stock spell already silences nearby enemies and energizes mana via its
|
||
// own effects; this script adds energy, rage, and runic power energize on
|
||
// top when the caster is class 12, so a single button refunds whichever
|
||
// resource pool the player is actually using. Non-Paragon casters are
|
||
// untouched and keep learning their stock racial variant.
|
||
class spell_paragon_arcane_torrent : public SpellScript
|
||
{
|
||
PrepareSpellScript(spell_paragon_arcane_torrent);
|
||
|
||
void HandleAfterCast()
|
||
{
|
||
Unit* caster = GetCaster();
|
||
if (!caster || !caster->IsPlayer())
|
||
return;
|
||
|
||
Player* player = caster->ToPlayer();
|
||
if (player->getClass() != CLASS_PARAGON)
|
||
return;
|
||
|
||
// Stock energize amounts from spell_dbc:
|
||
// 25046 Arcane Torrent (Energy) -> 15 energy
|
||
// 50613 Arcane Torrent (Runic Power) -> 15 displayed RP (= 150
|
||
// internal; AC stores RP scaled 10x, see Player::SetMaxPower
|
||
// POWER_RUNIC_POWER, 1000).
|
||
// Rage uses the same 10x internal scaling as runic power (see
|
||
// Player.cpp:Regenerate where rage decay is `-20` for "2 rage by
|
||
// tick"), so 15 displayed rage = 150 internal.
|
||
// ModifyPower no-ops on pools the player has no max for, so this is
|
||
// safe even before the Paragon picks up energy/rage/RP abilities.
|
||
constexpr int32 kEnergyGain = 15;
|
||
constexpr int32 kRageGain = 150;
|
||
constexpr int32 kRunicPowerGain = 150;
|
||
|
||
SpellInfo const* spellInfo = GetSpellInfo();
|
||
uint32 const spellId = spellInfo ? spellInfo->Id : 28730u;
|
||
|
||
caster->EnergizeBySpell(player, spellId, kEnergyGain, POWER_ENERGY);
|
||
caster->EnergizeBySpell(player, spellId, kRageGain, POWER_RAGE);
|
||
caster->EnergizeBySpell(player, spellId, kRunicPowerGain, POWER_RUNIC_POWER);
|
||
}
|
||
|
||
void Register() override
|
||
{
|
||
AfterCast += SpellCastFn(spell_paragon_arcane_torrent::HandleAfterCast);
|
||
}
|
||
};
|
||
|
||
void AddSC_paragon()
|
||
{
|
||
new Paragon_PlayerScript();
|
||
RegisterSpellScript(spell_paragon_arcane_torrent);
|
||
}
|