diff --git a/modules/mod-paragon/data/sql/db-world/base/paragon_spell_ae_cost.sql b/modules/mod-paragon/data/sql/db-world/base/paragon_spell_ae_cost.sql index fc5d62c..693ce21 100644 --- a/modules/mod-paragon/data/sql/db-world/base/paragon_spell_ae_cost.sql +++ b/modules/mod-paragon/data/sql/db-world/base/paragon_spell_ae_cost.sql @@ -21,6 +21,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (10, 1), (17, 1), (53, 1), + (66, 1), (72, 1), (75, 1), (78, 1), @@ -30,6 +31,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (118, 1), (120, 1), (122, 1), + (126, 1), (130, 1), (131, 1), (132, 1), @@ -52,6 +54,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (469, 1), (475, 1), (498, 1), + (526, 1), (527, 1), (528, 1), (543, 1), @@ -73,22 +76,28 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (676, 1), (686, 1), (687, 1), + (688, 1), (689, 1), + (691, 1), (693, 1), (694, 1), + (697, 1), (698, 1), (702, 1), (703, 1), (706, 1), (710, 1), + (712, 1), (740, 1), (755, 1), (759, 1), + (768, 1), (770, 1), (772, 1), (774, 1), (779, 1), (781, 1), + (783, 1), (845, 1), (853, 1), (871, 1), @@ -104,6 +113,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (1038, 1), (1044, 1), (1064, 1), + (1066, 1), (1079, 1), (1082, 1), (1098, 1), @@ -176,6 +186,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (2812, 1), (2825, 1), (2893, 1), + (2894, 1), (2908, 1), (2912, 1), (2944, 1), @@ -194,6 +205,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (3565, 1), (3566, 1), (3567, 1), + (3714, 1), (3738, 1), (4987, 1), (5116, 1), @@ -205,7 +217,9 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (5185, 1), (5209, 1), (5211, 1), - (5215, 1), + (5215, 1); + +INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (5217, 1), (5221, 1), (5225, 1), @@ -215,11 +229,10 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (5308, 1), (5384, 1), (5484, 1), + (5487, 1), (5500, 1), (5502, 1), - (5504, 1); - -INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES + (5504, 1), (5675, 1), (5676, 1), (5697, 1), @@ -244,6 +257,8 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (6770, 1), (6785, 1), (6789, 1), + (6795, 1), + (6807, 1), (6940, 1), (7294, 1), (7302, 1), @@ -283,6 +298,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (11418, 1), (11419, 1), (11420, 1), + (12051, 1), (13159, 1), (13161, 1), (13163, 1), @@ -297,6 +313,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (16857, 1), (16914, 1), (18499, 1), + (19263, 1), (19740, 1), (19742, 1), (19746, 1), @@ -323,7 +340,6 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (20252, 1), (20484, 1), (20736, 1), - (21084, 1), (21562, 1), (21849, 1), (22568, 1), @@ -331,6 +347,8 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (22812, 1), (22842, 1), (23028, 1), + (23161, 1), + (23214, 1), (23920, 1), (23922, 1), (24275, 1), @@ -349,6 +367,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (29722, 1), (29858, 1), (29893, 1), + (30449, 1), (30451, 1), (30455, 1), (30482, 1), @@ -372,12 +391,14 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (33745, 1), (33763, 1), (33786, 1), + (33943, 1), (34026, 1), (34074, 1), (34428, 1), (34433, 1), (34477, 1), (34600, 1), + (34767, 1), (35715, 1), (35717, 1), (36936, 1), @@ -398,7 +419,9 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (47541, 1), (47568, 1), (47897, 1), - (48018, 1), + (48018, 1); + +INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (48020, 1), (48045, 1), (48263, 1), @@ -416,14 +439,19 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (49576, 1), (49998, 1), (50464, 1), + (50769, 1), (50842, 1), + (51505, 1), + (51514, 1), (51722, 1), (51723, 1), - (52610, 1); - -INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES + (51730, 1), + (52127, 1), + (52610, 1), (53140, 1), (53142, 1), + (53271, 1), + (53351, 1), (53407, 1), (53408, 1), (53600, 1), @@ -432,15 +460,23 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES (54428, 1), (55342, 1), (55694, 1), + (56222, 1), (56641, 1), + (56815, 1), + (57330, 1), (57755, 1), (57934, 1), (57994, 1), (60192, 1), (61846, 1), + (61999, 1), (62078, 1), (62124, 1), (62757, 1), (64382, 1), - (64843, 1); + (64843, 1), + (64901, 1), + (66842, 1), + (66843, 1), + (66844, 1); diff --git a/modules/mod-paragon/data/sql/db-world/updates/2026_05_10_05.sql b/modules/mod-paragon/data/sql/db-world/updates/2026_05_10_05.sql new file mode 100644 index 0000000..0b874f7 --- /dev/null +++ b/modules/mod-paragon/data/sql/db-world/updates/2026_05_10_05.sql @@ -0,0 +1,62 @@ +-- mod-paragon: backfill paragon_spell_ae_cost rows for spells newly exposed +-- by the Character Advancement panel after removing the over-aggressive +-- ClassMask=0 filter from tools/_gen_paragon_advancement_spells_lua.py. +-- +-- The base file (data/sql/db-world/base/paragon_spell_ae_cost.sql) was +-- regenerated alongside this migration so fresh deployments already have +-- these rows. Existing servers do not re-run base files on content change, +-- so this update inserts the new (spell_id, ae_cost) pairs idempotently. +-- INSERT IGNORE keeps any per-row tuning a server operator may have already +-- applied to spell_ids that happen to overlap. +-- +-- New ids include: 51505 Lava Burst (Shaman), 12051 Evocation / 1066 Aqueous +-- Form / Hex / Mage Ward / Spellsteal (Mage), 53351 Kill Shot / 19263 +-- Deterrence / 53271 Master's Call (Hunter), 3714 Path of Frost / 57330 +-- Horn of Winter / 56815 Rune Strike / 61999 Raise Ally / 56222 Dark Command +-- (DK), and 39 other trainer-taught class abilities whose stock +-- SkillLineAbility.dbc rows have ClassMask=0 (the skill line itself pins the +-- class for these rows; ClassMask is redundant on class-spec lines). + +INSERT IGNORE INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES + (66, 1), -- Invisibility (Mage) + (126, 1), -- Eye of Kilrogg (Warlock) + (526, 1), -- Cure Toxins (Shaman) + (688, 1), -- Summon Imp (Warlock) + (691, 1), -- Summon Felhunter (Warlock) + (697, 1), -- Summon Voidwalker (Warlock) + (712, 1), -- Summon Succubus (Warlock) + (768, 1), -- Cat Form (Druid) + (783, 1), -- Travel Form (Druid) + (1066, 1), -- Aqueous Form (Mage) + (2894, 1), -- Fire Resistance Totem (Shaman) + (3714, 1), -- Path of Frost (DK) + (5215, 1), -- Prowl (Druid) + (5487, 1), -- Bear Form (Druid) + (5504, 1), -- Conjure Refreshment (Mage) + (6795, 1), -- Growl (Druid) + (6807, 1), -- Maul (Druid) + (12051, 1), -- Evocation (Mage) + (19263, 1), -- Deterrence (Hunter) + (23161, 1), -- Summon Dreadsteed (Warlock) + (23214, 1), -- Summon Charger (Paladin) + (30449, 1), -- Spellsteal (Mage) + (33943, 1), -- Flight Form (Druid) + (34767, 1), -- Summon Felguard (Warlock) + (48018, 1), -- Demonic Circle: Summon (Warlock) + (50769, 1), -- Revive (Druid) + (51505, 1), -- Lava Burst (Shaman) + (51514, 1), -- Hex (Shaman) + (51730, 1), -- Earthliving Weapon (Shaman) + (52127, 1), -- Water Shield (Shaman) + (52610, 1), -- Savage Roar (Druid) + (53271, 1), -- Master's Call (Hunter) + (53351, 1), -- Kill Shot (Hunter) + (56222, 1), -- Dark Command (DK) + (56815, 1), -- Rune Strike (DK) + (57330, 1), -- Horn of Winter (DK) + (61999, 1), -- Raise Ally (DK) + (64843, 1), -- Divine Hymn (Priest) + (64901, 1), -- Hymn of Hope (Priest) + (66842, 1), -- Call of the Elements (Shaman totem set) + (66843, 1), -- Call of the Ancestors (Shaman totem set) + (66844, 1); -- Call of the Spirits (Shaman totem set) diff --git a/modules/mod-paragon/src/Paragon_Essence.cpp b/modules/mod-paragon/src/Paragon_Essence.cpp index 56e36ed..016362b 100644 --- a/modules/mod-paragon/src/Paragon_Essence.cpp +++ b/modules/mod-paragon/src/Paragon_Essence.cpp @@ -1203,10 +1203,21 @@ void PanelLearnSpellChain(Player* pl, uint32 baseSpellId) // spellbook icons. The correct *passive* spellbook entries the // player is supposed to see are 59879 / 59921 (the descriptive // "Passive disease" rows; SPELL_ATTR0_PASSIVE bit set). + // After the Paragon class-skill cascade guard landed in + // Player::learnSkillRewardedSpells, NONE of the DK skill-line + // cascade rewards are auto-granted any more — so passives that + // used to ride along on a class skill cascade (Forceful + // Deflection on Blood Strike, Runic Focus on Icy Touch) must be + // explicitly attached here, the same way Blood Plague / Frost + // Fever are. Add new entries when a panel-purchased active is + // expected to come with a passive spellbook entry that no + // SPELL_EFFECT_LEARN_SPELL on the parent provides. struct AttachedPassive { uint32 parentHead; uint32 attachedSpell; }; static AttachedPassive const kAttached[] = { { 45462, 59879 }, // Plague Strike -> Blood Plague (passive entry) - { 45477, 59921 }, // Icy Touch -> Frost Fever (passive entry) + { 45477, 59921 }, // Icy Touch -> Frost Fever (passive entry) + { 45477, 61455 }, // Icy Touch -> Runic Focus (parry from spell power) + { 45902, 49410 }, // Blood Strike -> Forceful Deflection (parry from strength) }; // Self-heal: a previous build of mod-paragon (briefly shipped) @@ -3889,16 +3900,25 @@ public: lb.child, lb.parent, player->GetName()); } } - // 2b) Re-attach the correct passive spellbook entry (59879 / - // 59921) for any panel-purchased Plague Strike / Icy Touch - // that's missing it. `learnSpell` here can re-fire the DK - // skill-line cascade and re-grant Blood Presence / Death - // Coil / Death Grip / Forceful Deflection — Step 3's - // scoped sweep is what cleans those up. + // 2b) Re-attach the correct passive spellbook entries for any + // panel-purchased parent that is missing them. After the + // class-skill cascade guard in + // Player::learnSkillRewardedSpells, the cascade no longer + // fires for Paragons, so these attachments are the ONLY + // source for the disease passive icons (Blood Plague / + // Frost Fever) and the small DK weapon passives (Forceful + // Deflection from Blood Strike, Runic Focus from Icy + // Touch). Existing characters predating the guard may + // have FD/RF in their spellbook from the cascade but no + // panel_spell_children row tying them to the parent; + // re-running learnSpell when they already have the spell + // just records the child row and is a no-op otherwise. struct LegacyFix { uint32 parent; uint32 correctChild; }; static LegacyFix const kFixup[] = { - { 45462, 59879 }, - { 45477, 59921 }, + { 45462, 59879 }, // Plague Strike -> Blood Plague (passive) + { 45477, 59921 }, // Icy Touch -> Frost Fever (passive) + { 45477, 61455 }, // Icy Touch -> Runic Focus + { 45902, 49410 }, // Blood Strike -> Forceful Deflection }; for (auto const& lf : kFixup) { diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index f5d286f..e9319e4 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -12017,6 +12017,28 @@ void Player::learnSkillRewardedSpells(uint32 skill_id, uint32 skill_value) uint32 raceMask = getRaceMask(); uint32 classMask = getClassMask(); + // Fractured / Paragon: the Character Advancement panel is the sole + // authority over which class abilities a Paragon owns. The skill-line + // cascade 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 of those re-grants every SLA-tagged class ability on the + // matching skill line — leaking Blood Presence / Death Coil / Death + // Grip / etc. back into the spellbook within seconds even after the + // player intentionally refunded them via the panel. Skip the cascade + // for class-category skill lines on Paragon characters; mod-paragon + // calls Player::learnSpell directly for the abilities the player + // actually purchased, including their attached passives. Profession, + // 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) + { + if (SkillLineEntry const* sl = sSkillLineStore.LookupEntry(skill_id)) + if (sl->categoryId == SKILL_CATEGORY_CLASS) + return; + } + // Get all abilities for this skill and sort by MinSkillLineRank (lowest to highest) auto abilities = GetSkillLineAbilitiesBySkillLine(skill_id); std::vector sortedAbilities(abilities.begin(), abilities.end());