* Weapon-narrow: replace blanket EquippedItemSubClassMask bypass with an
explicit allowlist (IsParagonWeaponSubclassWildcardSpell, currently
Maelstrom Weapon 51528 only). Applied at all three gates: Player::
HasItemFitToSpellRequirements, Player::CheckAttackFitToAuraRequirement,
and Aura::IsProcTriggeredOnEvent. Maelstrom Weapon still procs from any
weapon for Paragon, but Hack and Slash / Sword / Mace Specialization
remain correctly weapon-gated.
* Talent ability-rank cascade: TeachLevelGatedAbilityChainNoPanel +
CascadeRanksForTalentLearnSpellEffects walk the SPELL_EFFECT_LEARN_SPELL
chain a talent rank grants and learn each rank up to the player's level.
Wired into HandleCommit (on talent purchase) and OnPlayerLevelChanged
(on level-up). Fixes Mangle (and any future LEARN_SPELL talent) being
stuck at rank 1 because Player::learnSkillRewardedSpells is intentionally
disabled for Paragon's class skill lines.
* Riding-skill gate for flight forms (IsParagonSpellAllowedByRidingSkill):
Flight Form (33943) requires Expert Riding (34090); Swift Flight Form
(40120) requires Artisan Riding (34091). Applied in PanelLearnSpellChain
and TeachLevelGatedAbilityChainNoPanel so the cascade can't push past a
rank the player isn't trained for. Also backticks `rank` in the level-up
query (MySQL reserved word).
* Savage Defense (62600) on the panel: the SpellData bake's blanket
SPELL_ATTR0_PASSIVE filter dropped Savage Defense even though Druid
trainer 33 sells it at level 40. Bake (in fractured-tooling) now carves
out a small PASSIVE_TRAINER_ALLOWLIST and the regenerated
paragon_spell_ae_cost.sql + 2026_05_11_05.sql migration surface it on
the Druid Feral spell tab.
Co-authored-by: Cursor <cursoragent@cursor.com>
The skill-line cascade in Player::learnSkillRewardedSpells re-fires from
_LoadSkills (every login), UpdateSkillsForLevel (every level-up),
UpdateSkillPro (every weapon-skill tick on a training dummy), and
SetSkill (first time a class skill is granted). Each pass re-grants
every SkillLineAbility-tagged class ability on the matching skill line,
which leaks Blood Presence / Death Coil / Death Grip / etc. back into
the spellbook within seconds even after the player intentionally
refunded them via the Character Advancement panel.
Path B fix: a 5-line guard at the top of learnSkillRewardedSpells skips
the cascade for class-category skill lines on CLASS_PARAGON characters.
mod-paragon already calls Player::learnSpell directly for the abilities
the player actually purchased (and their attached passives), so the
panel becomes the sole authority over class abilities. Profession,
weapon, language, and racial cascades stay enabled so recipe auto-learn,
weapon proficiencies, and racial perks still work.
Side effect: passives that previously rode along on the cascade
(Forceful Deflection on Blood Strike, Runic Focus on Icy Touch) must be
force-attached the same way Blood Plague / Frost Fever already are.
Extend kAttached and kFixup in Paragon_Essence.cpp to do that; existing
characters self-heal on next login.
Backfill paragon_spell_ae_cost for 42 spells newly exposed by the panel
after the ClassMask=0 filter was removed from the client catalog
generator (Lava Burst, Hex, Evocation, Kill Shot, Path of Frost,
Horn of Winter, Rune Strike, Raise Ally, Dark Command, etc.). Migration
is INSERT IGNORE so any per-spell tuning on existing rows is preserved.
Co-authored-by: Cursor <cursoragent@cursor.com>
- PanelLearnSpellChain: record every non-chain passive as panel_spell_child;
only revoke non-passive (Blood Presence, Death Coil, Death Grip, etc.).
- RevokeUnwantedCascadeSpellsForPlayer: skip passive rewards on login sweep.
- RevokeBlockedSpellsForPlayer: migrate legacy passive revoke rows to
children; walk (parent, revoked) pairs from DB.
- PruneSkillLineCascadeChildrenFromDb: only strip actives wrongly stored as
children; never strip passives.
- SpellInfoCorrections: set SPELL_ATTR0_PASSIVE on Forceful Deflection (49410)
and Runic Focus (61455) so IsPassive() matches spellbook behavior.
- PanelUnlearnTalentPurchase: mirror resetTalents (_removeTalentAurasAndSpells,
_removeTalent, SendTalentsInfoData) so Beast Mastery loss triggers pet reset.
- OnPlayerLogin: run legacy passive attach before scoped cascade sweep.
- Add .paragon recalibrate GM command (RBAC modify): full panel reset + AE/TE
reconciliation for selected player or self.
Co-authored-by: Cursor <cursoragent@cursor.com>
Two related additions to mod-paragon:
* HandleCommit gains a third payload section, " u:<id>,...", carrying
spell IDs the player wants to refund + unlearn in the same commit
that learns / talents through. The protocol stays backward-compat
(older clients omit the section). PanelUnlearnSpellPurchase mirrors
the per-spell branch of HandleParagonResetAbilities: tracked passive
children are removed first, then the chain head, then panel_spells /
panel_spell_children / panel_spell_revoked rows for that purchase
are dropped, then LookupSpellAECost(head) is refunded into the
cache. Unlearns are applied before learns inside the commit so the
refund covers the same-commit spends. Allow-list for the silence
window now includes chain ranks + panel_spell_children for the
intentional unlearns so "You have unlearned X" toasts stay visible
for the targeted spell while cascade dependents stay silenced.
* ReconcileEssenceForPlayer reads panel_spells + panel_talents and
sets the cache to ComputeStartingAE/TE(level) - sum-of-spends.
Self-heals drift in either direction: clamps the cache down when
the player has more essence than their level + spends allow
(cheese clamp), and tops up when they have less (admin-tweak /
crash recovery). Wired into OnPlayerLogin (after LoadCurrencyFromDb,
before PushCurrency so the first balance the client sees is the
reconciled one) and OnPlayerLevelChanged (replaces the old
GrantLevelUpEssence delta -- Reconcile sets the absolute correct
balance from level + spend, so it subsumes the per-level grant and
the cheese clamp in one call). Costs come from the same
paragon_spell_ae_cost / config keys HandleCommit uses so the math
stays in lockstep across any future cost rebalance.
Both features ship in patch-enUS-6.MPQ v0.9.16: right-click a learned
spell row to queue an unlearn (header shows +N AE refund preview) and
hit Learn All to apply. The icon picker also got two fixes -- the
leading INV_Misc_QuestionMark is no longer duplicated, and the
selection ring is now a tooltip-border Frame anchored to the cell
bounds (the prior UI-ActionButton-Border texture rendered nearly
invisible at non-native sizes).
Co-authored-by: Cursor <cursoragent@cursor.com>
Server side of the v0.7.10 Builds drop. Squashes a few footguns from
the original Builds catalog and adds a one-click "save what I have
right now" path the Overview pane can hook directly into.
- HandleBuildSaveCurrent: new C BUILD SAVE_CURRENT verb. Inserts a
fresh build row, snapshots the live panel into its recipe, sets it
active. No AE/TE motion, no relearning -- just a named slot for
whatever the player already has.
- Reset abilities / Reset talents now SetActiveBuildId(0) and re-push
the catalog. Without this, the next swap silently overwrote the
active build's saved recipe with the (now empty/partial) post-reset
state -- effectively erasing the build.
- Delete of the *active* build is now a hard reset (HandleParagonResetAll):
unlearn everything the panel bought, refund all AE/TE. Deleting a
non-active slot still just removes the saved recipe row + parked pet.
- Load of the currently-active build is now a "revert to last snapshot"
instead of a no-op refresh: keeps the saved recipe authoritative,
parks the pet, resets, re-applies. Useful for discarding pending
edits.
- After a successful Learn All while a build is active: archive the
build's previous share_code + recipe into
character_paragon_build_share_archive* (so codes already posted to
Discord keep importing the frozen loadout), snapshot the new panel
into the live build, assign a fresh share_code, push catalog.
- HandleBuildImport now falls back to the archive tables when a code
isn't in the live catalog -- old shared codes resurrect the recipe
they pointed at when they were retired.
- Imports never copy pet_number (the parked pet belongs to the source
player); if the imported recipe contains Tame Beast we hint that the
importer needs to tame their own pet.
- BuildPanelOwnedSpellsAllowlist now walks SPELL_EFFECT_LEARN_SPELL
effects on talent rank spells (Mangle, Feral Charge, Mutilate, ...)
so the login cascade sweep stops revoking talent-granted active
abilities.
Schema: new mod-paragon migration 2026_05_10_05.sql adds
character_paragon_build_share_archive (+ _spells / _talents).
Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace the "favorite" toggle with import-by-share-code: every build
gets a 6-char realm-unique alphanumeric code on creation; pasting one
into the BuildsPane share box copies the recipe (name + icon + spells
+ talents) into the importer's catalog as a new build, with a fresh
share code so the imported copy can be re-shared independently.
- Add C BUILD UNLOAD verb so the client can clear a stale active-build
pointer without forcing a swap. Wired to a new "Unload (clear active)"
right-click context menu entry on the active build.
- Per-build tooltip now shows "Remaining if loaded: X AE / Y TE",
computed server-side as total_earned - recipe_cost. Negative renders
red so the player sees insufficient-currency cases before clicking
Load. Suppressed for the active build (HandleBuildLoad short-circuits
on target == active so the line would be misleading).
- Schema migration 2026_05_10_04.sql: drop is_favorite from
character_paragon_builds and add share_code CHAR(6) UNIQUE NULL with
lazy backfill on every PushBuildCatalog (so pre-migration rows pick
up codes the first time the player opens the panel).
Co-authored-by: Cursor <cursoragent@cursor.com>
Server-side Character Advancement now stores named, icon-tagged build
recipes (panel-purchased spells + per-spec talent ranks) and atomically
swaps between them by snapshotting the active build, refunding AE/TE
through HandleParagonReset{Talents,Abilities}, and re-spending on the
target recipe. Hunter pets attached to a build are parked to
PET_SAVE_NOT_IN_SLOT (mirroring HandleStableSwapPet) so name, talents,
and exp survive swaps; non-hunter pets (warlock demon, DK ghoul, mage
water elemental) are NOT parked because the engine resummons them from
a fresh template each cast.
New PARAA verbs: Q BUILDS / C BUILD NEW / C BUILD EDIT / C BUILD
DELETE / C BUILD FAVORITE / C BUILD LOAD. The catalog is pushed on
login and after every mutation as a single addon message.
Schema (mod-paragon migration 2026_05_10_03.sql):
- character_paragon_builds (build_id PK, guid, name, icon, is_favorite,
pet_number, created_at)
- character_paragon_build_spells (build_id, spell_id)
- character_paragon_build_talents (build_id, spec, talent_id, rank)
- character_paragon_active_build (guid PK, build_id)
The talent recipe table is spec-keyed so a build remembers tank/dps
dual-spec layouts independently. Swaps are blocked while in combat.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
- SpellInfoCorrections: zero Reagent/ReagentCount on spells with non-zero
SpellFamilyName so class abilities no longer require shards, candles,
etc., while profession crafts (SpellFamilyName 0) keep mats. Matches
the client Spell.dbc bake in patch-enUS-4.MPQ.
- Paragon_SC: OnPlayerIsClass returns true for CLASS_CONTEXT_EQUIP_RELIC
for paladin/druid/shaman/warlock/dk so Paragon can equip all relic types
in the ranged slot.
- CLIENT-PATCHES: document Spell.dbc reagent pass, rune script order, and
stock ammo slot behavior in patch-enUS-5.
Co-authored-by: Cursor <cursoragent@cursor.com>
Stock 3.3.5 hardcodes per-class stat -> AP/SP formulas in
Player::UpdateAttackPowerAndDamage and Unit::SpellBase{Damage,Healing}BonusDone,
so class 12 fell into the default branches and ended up with 0 AP and 0 SP
regardless of STR / AGI / INT / SPI. The character sheet, combat log, and
ability damage all reflected this, and Mental Quickness-style AP->SP plumbing
silently no-oped on Paragon characters.
Add Paragon-specific branches in core (no PlayerScript hooks - those caused
SIGSEGVs when the new mid-list enum entry shifted later hook ordinals and
broke vtable dispatch):
- StatSystem.cpp: melee and ranged AP = level*2 + STR + AGI - 20, mirroring
the formula the UI patch already advertises in tooltips.
- Unit.cpp: intrinsic SP = level*2 + INT + SPI - 20 (clamped >=0),
added symmetrically to SpellBaseDamageBonusDone and
SpellBaseHealingBonusDone so the single advertised Spell Power value the
character sheet renders matches what spells actually use in combat.
Drop the now-unused UnitDefines.h include in Paragon_SC.cpp - it was only
needed by the AP PlayerScript hook that was rolled back in favor of the
core change.
Co-authored-by: Cursor <cursoragent@cursor.com>
RevokeUnwantedCascadeSpellsForPlayer and RevokeBlockedSpellsForPlayer
built their allowlist only from character_paragon_panel_spells and
panel_spell_children. Many Character Advancement "abilities" (e.g.
Scourge Strike) are panel talents stored in character_paragon_panel_talents,
so learning Death Coil afterward activated DK skill lines and the sweep
removed those spells as false orphans.
Add BuildPanelOwnedSpellsAllowlist to union spell chains, talent rank spell
IDs up to the purchased rank, and passive children. Also keep the prior
fixes: clear stale panel_spell_revoked rows on purchase and skip+delete
revoke entries that now match the allowlist on login.
Co-authored-by: Cursor <cursoragent@cursor.com>
Paragon OnPlayerHasActivePowerType only reported POWER_RAGE when
Paragon.MultiResource.HasActivePowers was true. Core melee rage uses
Unit::DealDamage -> HasActivePowerType(POWER_RAGE) before RewardRage;
missing module config (common on fresh clones / Docker without merged
mod_paragon.conf) fell through to GetOption(..., false) and white swings
never generated rage. Match mod_paragon.conf.dist and default the C++
fallback to true so Paragon behaves correctly out of the box. Set
Paragon.MultiResource.HasActivePowers = 0 only for intentional test builds.
Co-authored-by: Cursor <cursoragent@cursor.com>
Extend spell_paragon_arcane_torrent to EnergizeBySpell POWER_RAGE 150 (15
displayed; rage uses the same 10x internal scaling as runic power, see the
`-20` rage decay step in Player::Regenerate). Paragon's combined Arcane
Torrent now refunds mana, rage, energy, and runic power -- whichever pool
the character is using at the moment. ModifyPower no-ops on pools with
MaxPower == 0, so it's safe even before the Paragon picks up rage abilities.
Co-authored-by: Cursor <cursoragent@cursor.com>
Building on the previous fix that hid the rogue and DK Arcane Torrent variants
for Paragon Blood Elves: instead of just dropping the duplicates, turn the
remaining mana variant (28730) into a single combined racial that refunds
whichever resource pool the character is using at the moment.
Add SpellScript spell_paragon_arcane_torrent in modules/mod-paragon/src/
Paragon_SC.cpp. Hooks AfterCast on 28730: when the caster is class 12 the
script EnergizeBySpell's 15 energy and 150 internal runic power (= 15 displayed,
matching stock 25046 / 50613 amounts) on top of the spell's stock mana effect.
ModifyPower no-ops on pools the player has no max for, so it is safe even
before the Paragon picks up energy- or RP-using abilities. Non-Paragon Blood
Elves are untouched and keep learning their stock racial.
Update migration 2026_05_10_03.sql to also register the script binding via
spell_script_names (28730 -> 'spell_paragon_arcane_torrent'). Idempotent
DELETE + INSERT.
Co-authored-by: Cursor <cursoragent@cursor.com>
Blood Elf racial skill line 756 grants three different Arcane Torrent spell
IDs (28730 mana, 25046 rogue energy, 50613 DK runic power). The blanket
SkillLineAbility overlay in 2026_05_10_02 OR'd class 12 into all three, so
Paragon Blood Elves auto-learned every variant and the spellbook listed three
identical "Arcane Torrent" entries.
Add db-world migration 2026_05_10_03.sql to clear the class-12 bit on the rogue
and DK rows only (SkillLineAbility IDs 13338 and 17510), leaving 28730 as the
sole Paragon-visible racial cast. OnPlayerLogin removes 25046/50613 if still
present so existing characters self-heal without a manual unlearn.
The fractured-tooling DBC overlay generator is updated in the same workspace
to skip those two rows when regenerating SkillLineAbility SQL.
Co-authored-by: Cursor <cursoragent@cursor.com>
mod-paragon Paragon_Essence.cpp:
- Broaden SkillLinesLinkedToSpell: collect every SkillLineAbility row for
an anchor spell regardless of AcquireMethod, so anchor spells whose
primary SLA uses AcquireMethod 0 (e.g. Blood Strike) correctly identify
their skill lines and let the dependent classifier do its job.
- IsSpellSkillLineCascadeDependent / RevokeUnwantedCascadeSpellsForPlayer
use the broadened helper. HandleCommit calls the post-purchase sweep
immediately so the spellbook never carries lingering cascade dependents
(Blood Presence / Forceful Deflection / Death Coil / Death Grip).
- New character_paragon_panel_spell_revoked table tracks which active
dependents we've revoked per (guid, parent) so OnPlayerLogin can
re-revoke them after AC's _LoadSkills -> learnSkillRewardedSpells
silently re-grants them.
- OnPlayerLogin opens the client SILENCE window via SendSilenceOpenForCommit
with an empty allow list and intentionally omits the matching
SendSilenceClose: the chat frame buffers CHAT_MSG_SYSTEM during the
loading screen and only flushes after PLAYER_ENTERING_WORLD, so a paired
CLOSE would shut the filter before the buffered "you have unlearned X"
toasts hit it. The addon's 8s fail-open closes the window after the flush.
- New `.paragon hat` chat command for diagnosing Honor Among Thieves
triggers (talent rank, learned spell, applied aura, proc table entry).
mod-paragon Paragon_SC.cpp:
- OnPlayerUpdate pushes server-authoritative combo points to the client
via PARAA "R CP <n>" whenever the count changes. The client-side
ComboFrame Paragon simulator listens for this and updates the target
frame, fixing HAT-generated CP not displaying (HAT's trigger casts
with a null target, which the combat-log inference path can't see).
- OnPlayerUpdate also pushes "R RUNES <cd0..cd5>" (ms remaining per
rune slot) on rune mask changes, so the client RuneFrame simulator
stays in lock-step with Spell::TakeRunePower instead of drifting
through combat-log latency.
mod-paragon SQL:
- New updates/2026_05_09_00.sql migration creates
character_paragon_panel_spell_revoked for AC's auto-DBUpdater so a
fresh checkout can stand up an existing characters DB without
manual intervention. Matching CREATE TABLE IF NOT EXISTS in
base/character_paragon_panel_learned.sql for fresh installs.
mod-paragon conf:
- New Paragon.Diag.PanelLearn flag traces every PanelLearnSpellChain
commit (chain ids, before/after spell-map sizes, side-spell
classification) for diagnosing "spell reappears on relog" bugs.
Co-authored-by: Cursor <cursoragent@cursor.com>