Compare commits

...

5 Commits

Author SHA1 Message Date
Docker Build 87219cb4eb Paragon: multidot Devouring Plague, stance/presence clones, advancement SLA
- Allow multiple Devouring Plague DoTs on different targets (core + DK script).

- Warrior stance and DK presence clone spells for Character Advancement; spellbook SkillLineAbility rows and aura/shapeshift attribute fixes.

- World SQL updates 2026_05_12_02 through 07 (mod-paragon db-world).

Client patch-enUS-4/5/6 and Wow.exe ship on the matching GitHub Release (not in repo).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 19:20:13 -04:00
Docker Build da17074a63 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>
2026-05-12 04:49:38 -04:00
Docker Build b8826370c6 Paragon: cross-class stance exclusivity + cancel, hunter ammo soft-fail, Feral Cat scaling, Pestilence DP spread
Server-side batch following v0.7.18, all gated to Paragon (or applied
server-wide where the design discussion called for it):

* Cross-class stance / form / presence / aspect exclusivity (server-wide).
  New `IsFracturedExclusiveStanceSpell()` (`Unit.cpp`/`Unit.h`) returns
  true for the union of warrior stances + druid forms (combat AND utility:
  Travel, Aquatic, Flight, Swift Flight) + Ghost Wolf + base Stealth +
  Shadowform + Metamorphosis + DK Presences + Hunter Aspects (combat AND
  utility: Cheetah, Pack). `Aura::CanStackWith` (`SpellAuras.cpp`) refuses
  to stack two spells from this set, which routes through
  `_RemoveNoStackAurasDueToAura` to drop the older aura -- the same
  mechanism Battle Elixirs / Curses use. Plugs the stock-AC gap where DK
  Presences and Hunter Aspects (regular auras, just rendered in the
  stance bar) coexisted with engine-shapeshifts.

* Stances / Presences / Aspects cancellable like Druid forms.
  `SpellInfoCorrections.cpp` now zeroes `CategoryEntry` on warrior
  stances, DK presences, and every rank of every hunter aspect (moves
  them out of SpellCategory 47 "Combat States", which gates the client's
  right-click / `/cancelaura` path), AND clears `AttributesEx6` bit
  `0x1000` on warrior stances + DK presences (a second client-UI gate
  surfaced via DBC diff -- aspects don't have it set). Mirrored client-
  side by `_patch_spell_dbc_presences_cancelable.py`. Aspects / presences
  do NOT swap action bars (those are owned by `SPELL_AURA_MOD_SHAPESHIFT`,
  not by Category / AttrEx6) -- only warrior stances and druid forms keep
  the bar swap, matching the design requirement that presences/aspects
  not change the player's action bar.

* Hunter ammo soft-fail (server-wide).
  Replaced both `SPELL_FAILED_NO_AMMO` returns in `Spell::CheckCast` with
  `break;` so ranged + thrown abilities cast through with zero ammo;
  `_ApplyAmmoBonuses` continues to gate the actual arrow/bullet DPS bonus
  on a non-empty stack, so equipping ammo still pays off. New programmatic
  `ApplySpellFix`-style block in `SpellInfoCorrections.cpp` iterates every
  Hunter-family spell whose `EquippedItemClass == ITEM_CLASS_WEAPON` and
  `EquippedItemSubClassMask` includes Bow/Gun/Crossbow and sets
  `EquippedItemClass = -1` (skipping a small DENYLIST of Quiver / Ammo
  Pouch passive haste auras + Aynasha's Bow + Legendary Bow Haste -- those
  are item-equip-driven and must keep gating on the ranged weapon being
  equipped). Server log: ">> Fractured: dropped EquippedItemClass on 196
  hunter shot abilities". Mirrored client-side by the new
  `_patch_spell_dbc_hunter_ammo.py` so the 3.3.5a client preflight stops
  blocking the cast packet with "Ammo needs to be in the paper doll ammo
  slot before it can be fired." `Spell::TakeAmmo` no longer clears
  `PLAYER_AMMO_ID` to 0 when the bag empties (defense in depth so a half-
  deployed pair degrades to soft-fail rather than hard-reject). Adds
  `#include "ItemTemplate.h"` for `ITEM_SUBCLASS_WEAPON_*`.

* Feral Cat scaling (server-wide, cat-only).
  `StatSystem.cpp` `UpdateAttackPowerAndDamage` FORM_CAT branch doubles
  the AGI coefficient (1.0 -> 2.0). `SpellAuraEffects.cpp` Master
  Shapeshifter FORM_CAT branch doubles the talent's bp before triggering
  48420 (R1: 2% -> 4% crit, R2: 4% -> 8%). FORM_BEAR / FORM_DIREBEAR /
  FORM_MOONKIN / FORM_TREE branches all left untouched so bear stays
  "already fine" per the resident Feral expert. Client tooltip drift on
  Cat Form (768) + Master Shapeshifter (48411 / 48412) + Pestilence
  (50842) handled by `_patch_spell_dbc_feral_tooltips.py`.

* Pestilence spreads / refreshes Devouring Plague for Paragon casters.
  `spell_dk.cpp` `spell_dk_pestilence::HandleScriptEffect` now also
  spreads (and Glyph-of-Disease refreshes) Priest Devouring Plague when
  `IsParagonWildcardCaller(caster)`. Uses `GetAuraOfRankedSpell(2944)` so
  the spread copy carries the caster's actual rank. Stock DKs cannot
  cast Devouring Plague at all, so this branch is a no-op for them.

* Dancing Rune Weapon: Paragon copies melee, not casts.
  `spell_dk.cpp` `spell_dk_dancing_rune_weapon::CheckProc` for Paragon
  callers requires `eventInfo.GetDamageInfo()` and
  `spellInfo->DmgClass == SPELL_DAMAGE_CLASS_MELEE`, so the ghostly
  weapon now copies cross-class melee strikes (Hamstring, Sinister Strike,
  Heart Strike, Frost Strike, ...) and auto-attacks instead of re-casting
  the DK's nukes. Stock DK gating below is untouched.

* Maelstrom Weapon: drop Arcane Blast from the allowlist.
  `SpellInfo.cpp` and `spell_shaman.cpp` allowlists now match Fireball
  and Frostbolt only -- Arcane Blast stacked with its own self-buff was
  too potent.

* `BALANCE-TODO.md` added under `contrib/fractured-dev-extras/` to
  capture the resident Feral expert's recommendation, the levers we
  considered, and the cat-only Master-Shapeshifter / AGI-doubling
  resolution we shipped, plus the next-lever knobs if field reports
  still flag cat as weak.

DBC patcher pipeline (lives outside the repo in `fractured-tooling/`)
documented run order: runes -> reagents -> stances -> presences_cancelable
-> hunter_ammo -> feral_tooltips -> _make_paragon_dbc_patch.

No SQL migrations.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 23:17:37 -04:00
Docker Build d1d68cb44a fix(scripts): update server paths for new VPS location
Start script now defaults to /home/fractured-panel/azeroth-server and
passes -c flags so binaries find configs regardless of compiled-in path.
Update script gains --prefix flag to override CMAKE_INSTALL_PREFIX
(persisted to conf/config.sh) during rebuild.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 19:44:03 -05:00
Docker Build 999f7e94bd Paragon: narrow weapon bypass; cascade talent ranks; Savage Defense
* 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>
2026-05-11 19:09:25 -04:00
24 changed files with 1249 additions and 126 deletions
@@ -0,0 +1,94 @@
# Fractured / Paragon — Balance Backlog
Open balance / scaling questions surfaced by play-testers that have not yet
been actioned. Each entry should record the *symptom*, the *suspected cause*
based on a quick code dive, the *option set* we discussed, and any *links*
to relevant code. Knock items off as they ship.
---
## Feral Cat scaling feels weak (2026-05-11)
**Reporter feedback:**
> "Weapons don't automatically feature feral AP on this server and nothing
> is currently rescaled resulting in a super low feral scale. We can either
> rescale their abilities or add the AP back to all weapons."
>
> Resident Feral expert: "this is not a bear issue unfortunately, I have
> 11k AP" / "Stam > AP and Armor > AP means this is completely a cat issue."
**What's actually happening on the server:**
Feral AP *is* being granted on weapons — `ItemTemplate::getFeralBonus`
(`src/server/game/Entities/Item/ItemTemplate.h`) synthesises it from the
weapon's DPS for any weapon with `INVTYPE_WEAPON / 2HWEAPON / WEAPONMAINHAND
/ WEAPONOFFHAND`:
```cpp
int32 bonus = int32((extraDPS + getDPS()) * 14.0f) - 767;
```
That's then routed through `Player::ApplyFeralAPBonus` (Player.cpp ~6896)
into `m_baseFeralAP`, which `Player::UpdateAttackPowerAndDamage` adds into
the cat / bear formulas in `src/server/game/Entities/Unit/StatSystem.cpp`
(~line 477):
```cpp
case FORM_CAT:
val2 = (level * mLevelMult) + STR*2 + AGI - 20 + weapon_bonus + m_baseFeralAP;
break;
case FORM_BEAR:
case FORM_DIREBEAR:
val2 = (level * mLevelMult) + STR*2 - 20 + weapon_bonus + m_baseFeralAP;
break;
```
So bear and cat get the same `m_baseFeralAP` — the only delta is `+ AGI` for
cat. The "bears feel fine, cats feel weak" complaint is real; it's because
bear damage is rage / proc / mitigation driven (Lacerate stack, Savage
Defense, Pulverize-style talents) while cat damage is much more
AP-coefficient driven (Shred, Mangle, Rake, Rip, FB).
**Options discussed (pick when we revisit):**
| ID | Lever | Pros | Cons |
|----|-------|------|------|
| A | Bump the cat AP formula (e.g. `+ AGI*1.5`) | Cleanest, very tunable, hits both auto-attacks and abilities | Blunt instrument, also affects PvP |
| B | Boost cat-form ability coefficients (Shred / Rake / Rip / Mangle / FB) via spellmod auras | Most retail-faithful, surgical | More moving parts, harder to communicate |
| C | Increase `getFeralBonus` payout for druid weapons (e.g. `* 18.0f` or drop the `-767` floor) | Single-line change | Buffs bears too — bears are already fine, would over-buff |
| D | New Cat-only passive "Predator's Edge" = `+X% physical damage in Cat Form` | Easy to balance, easy to communicate, easy to undo | Adds another aura to track |
**Recommendation when we pick this back up:** start with **D + small A**
D is the readable "+15-20% cat damage" knob, A is a backup if AP-scaling
abilities (Mangle / FB) still feel weak relative to bleeds. Both are
trivially tunable via a single config knob during play-testing.
Do **not** pick C — it over-buffs bears, which the Feral expert explicitly
said are already fine.
**Resolution (2026-05-11, second pass):** Per the resident Feral expert
("instead of adding a new passive, you could probably just increase Cat
Form's Master Shapeshifter value along with its tooltip, alongside buffing
the agi scaling") we shipped a hybrid of **A** and a *cat-only* knob that
sits next to **D** but reuses an existing aura instead of inventing a new
one:
* `StatSystem.cpp` `UpdateAttackPowerAndDamage` FORM_CAT branch now
reads `+ GetStat(STAT_AGILITY) * 2.0f` (stock 1.0). FORM_BEAR /
FORM_DIREBEAR / FORM_MOONKIN are untouched, so bear's "already fine"
state is preserved.
* `SpellAuraEffects.cpp` Master Shapeshifter FORM_CAT branch multiplies
the talent's value by 2 before triggering 48420 (cat-form aura).
Talent ranks: 2% -> 4% (R1), 4% -> 8% (R2) crit chance in Cat Form.
Bear / Moonkin / Tree branches still pass `bp` through unchanged.
* Client tooltip drift handled by
`fractured-tooling/from-workspace-root/_patch_spell_dbc_feral_tooltips.py`,
which appends a `[Fractured]` paragraph to the Description column of
Cat Form (768) and Master Shapeshifter ranks (48411 / 48412).
If field reports after this lands still say cat is weak, the next levers
are (in order): bump `2.0f` to `2.5f` in StatSystem.cpp, then bump the
Master Shapeshifter cat multiplier from `* 2` to `* 3` in
SpellAuraEffects.cpp, then -- only if those are exhausted -- revisit
**B** (per-ability spellmod coefficients).
+3
View File
@@ -6,6 +6,9 @@ AzerothCore. Upstream AzerothCore does not ship these paths.
Contents:
- BUILD-NATIVE.md — fork-specific native build notes (moved from repo root).
- BALANCE-TODO.md — open balance / scaling questions raised by play-testers
that have not yet been actioned (e.g. Feral Cat scaling). Knock items off
as they ship.
- CLAUDE.md — optional AI assistant context (moved from repo root).
- CLIENT-PATCHES.md — what ships in a Fractured client release (MPQs +
patched Wow.exe), where to download them (Releases page), and how
@@ -472,6 +472,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(61999, 1),
(62078, 1),
(62124, 1),
(62600, 1),
(62757, 1),
(64382, 1),
(64843, 1),
@@ -0,0 +1,19 @@
-- mod-paragon: surface Savage Defense (62600) on the Druid Feral spell tab
-- of the Character Advancement panel.
--
-- The bake (tools/_gen_paragon_advancement_spells_lua.py) used to drop every
-- SPELL_ATTR0_PASSIVE spell up front, even when the trainer explicitly sells
-- it. That filter was correct for class-internal triggers (Feline Grace, etc.)
-- but kicked out Savage Defense -- a passive that DRUID trainer 33 sells at
-- level 40 (trainer_spell.sql line 2457). Bake now carves out a small
-- PASSIVE_TRAINER_ALLOWLIST so legitimate trainer-taught passives survive.
--
-- The base file (data/sql/db-world/base/paragon_spell_ae_cost.sql) was
-- regenerated alongside this migration so fresh deployments already have
-- this row. Existing servers do not re-run base files on content change,
-- so this update inserts the new (spell_id, ae_cost) pair idempotently.
-- INSERT IGNORE keeps any per-row tuning a server operator may have already
-- applied.
INSERT IGNORE INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
(62600, 1); -- Savage Defense (Druid, trainer 33, level 40)
@@ -0,0 +1,21 @@
-- Fractured / Paragon: multidot Devouring Plague clone (spell IDs 951000-951008).
-- Spell rows live in the patched client Spell.dbc (see fractured-tooling
-- from-workspace-root/_patch_spell_dbc_paragon_multidot_devouring_plague.py).
-- Deploy the same Spell.dbc into the worldserver `data/dbc/` folder OR import
-- equivalent `spell_dbc` rows from a full exporter; stock SQL cannot express
-- the SpellEntryfmt NA padding columns safely in one INSERT here.
DELETE FROM `spell_ranks` WHERE `first_spell_id` = 951000;
INSERT INTO `spell_ranks` (`first_spell_id`,`spell_id`,`rank`) VALUES
(951000,951000,1),
(951000,951001,2),
(951000,951002,3),
(951000,951003,4),
(951000,951004,5),
(951000,951005,6),
(951000,951006,7),
(951000,951007,8),
(951000,951008,9);
DELETE FROM `paragon_spell_ae_cost` WHERE `spell_id` IN (2944,951000);
INSERT INTO `paragon_spell_ae_cost` (`spell_id`,`ae_cost`) VALUES (951000, 1);
@@ -0,0 +1,15 @@
-- Fractured / Paragon: spellbook tab for multidot Devouring Plague (951000 chain).
-- Shadow priest skill line (78); ClassMask 2064 matches mod-paragon SLA overlay.
-- Client: patched SkillLineAbility.dbc in patch-enUS-4 from the same script.
DELETE FROM `skilllineability_dbc` WHERE `ID` IN (1951000, 1951001, 1951002, 1951003, 1951004, 1951005, 1951006, 1951007, 1951008);
INSERT INTO `skilllineability_dbc` (`ID`,`SkillLine`,`Spell`,`RaceMask`,`ClassMask`,`ExcludeRace`,`ExcludeClass`,`MinSkillLineRank`,`SupercededBySpell`,`AcquireMethod`,`TrivialSkillLineRankHigh`,`TrivialSkillLineRankLow`,`CharacterPoints_1`,`CharacterPoints_2`) VALUES
(1951000,78,951000,0,2064,0,0,1,0,0,0,0,0,0),
(1951001,78,951001,0,2064,0,0,1,0,0,0,0,0,0),
(1951002,78,951002,0,2064,0,0,1,0,0,0,0,0,0),
(1951003,78,951003,0,2064,0,0,1,0,0,0,0,0,0),
(1951004,78,951004,0,2064,0,0,1,0,0,0,0,0,0),
(1951005,78,951005,0,2064,0,0,1,0,0,0,0,0,0),
(1951006,78,951006,0,2064,0,0,1,0,0,0,0,0,0),
(1951007,78,951007,0,2064,0,0,1,0,0,0,0,0,0),
(1951008,78,951008,0,2064,0,0,1,0,0,0,0,0,0);
@@ -0,0 +1,21 @@
-- Fractured / Paragon: Character Advancement stance/presence clones (951010-951015).
-- Client: patched Spell.dbc + SpellShapeshiftForm.dbc + SkillLineAbility.dbc in patch-enUS-4.MPQ.
-- Server: copy Spell.dbc + SpellShapeshiftForm.dbc into `data/dbc/` (SpellShapeshiftForm is not in stock MPQ); SkillLineAbility is DB-driven on server.
DELETE FROM `paragon_spell_ae_cost` WHERE `spell_id` IN (951010,951011,951012,951013,951014,951015);
INSERT INTO `paragon_spell_ae_cost` (`spell_id`,`ae_cost`) VALUES
(951010, 1),
(951011, 1),
(951012, 1),
(951013, 1),
(951014, 1),
(951015, 1);
DELETE FROM `skilllineability_dbc` WHERE `ID` IN (1951020,1951021,1951022,1951023,1951024,1951025);
INSERT INTO `skilllineability_dbc` (`ID`,`SkillLine`,`Spell`,`RaceMask`,`ClassMask`,`ExcludeRace`,`ExcludeClass`,`MinSkillLineRank`,`SupercededBySpell`,`AcquireMethod`,`TrivialSkillLineRankHigh`,`TrivialSkillLineRankLow`,`CharacterPoints_1`,`CharacterPoints_2`) VALUES
(1951020,26,951010,0,2049,0,0,1,0,2,0,0,0,0),
(1951021,257,951011,0,2049,0,0,1,0,0,0,0,0,0),
(1951022,256,951012,0,2049,0,0,1,0,0,0,0,0,0),
(1951023,770,951013,0,2080,0,0,1,0,2,0,0,0,0),
(1951024,771,951014,0,2080,0,0,1,0,0,0,0,0,0),
(1951025,772,951015,0,2080,0,0,1,0,0,0,0,0,0);
@@ -0,0 +1,9 @@
-- Fractured / Paragon: run spell_dk_presence on Character Advancement DK presence clones (951013-951015).
-- Spell.dbc sets SpellFamilyName=0 on these rows (see fractured-tooling/_patch_spell_dbc_paragon_stance_presence_clones.py)
-- so the stock client does not map them onto DK stance buttons; core still needs the aura script for Improved Presence.
DELETE FROM `spell_script_names` WHERE `spell_id` IN (951013, 951014, 951015);
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
(951013, 'spell_dk_presence'),
(951014, 'spell_dk_presence'),
(951015, 'spell_dk_presence');
@@ -0,0 +1,8 @@
-- Fractured / Paragon: Character Advancement stance/presence clones — spellbook + client bits.
-- 1) SkillLineAbility: DK presence clones belong on 770/771/772 (Blood/Frost/Unholy tabs), not 760 (General).
-- (760 was an experiment; stance bar visibility is driven by Spell.dbc AttributesEx2 USE_SHAPESHIFT_BAR.)
-- 2) Idempotent if rows already match.
UPDATE `skilllineability_dbc` SET `SkillLine` = 770 WHERE `ID` = 1951023 AND `Spell` = 951013;
UPDATE `skilllineability_dbc` SET `SkillLine` = 771 WHERE `ID` = 1951024 AND `Spell` = 951014;
UPDATE `skilllineability_dbc` SET `SkillLine` = 772 WHERE `ID` = 1951025 AND `Spell` = 951015;
+315 -8
View File
@@ -584,6 +584,37 @@ void RevokeBlockedSpellsForPlayer(Player* pl)
continue;
}
// Same migration for meta-skill cascade spells. Earlier builds
// (and this one until just now) revoked the rune-enchant spells
// (Razorice, Cinderglacier, Rune of the Fallen Crusader, ...)
// when a Paragon learned Runeforging via the panel, because
// they're active spells and the default classifier treats
// unknown active cascades as leaks. New policy: anything on
// SKILL_RUNEFORGING is part of the Runeforging meta-skill
// package and stays. Drop the revoked row and, if we have a
// still-owned parent (typically Runeforging itself, 53428),
// re-record as a child so refund/unlearn still cleans them up.
bool isMetaSkillRevoke = false;
{
auto bounds = sSpellMgr->GetSkillLineAbilityMapBounds(sid);
for (auto it = bounds.first; it != bounds.second; ++it)
{
if (it->second->SkillLine == SKILL_RUNEFORGING)
{
isMetaSkillRevoke = true;
break;
}
}
}
if (isMetaSkillRevoke)
{
if (parent && ownedPanelSpells.count(parent))
passiveMigrate.emplace_back(parent, sid);
passiveStaleAll.push_back(sid);
++migrated;
continue;
}
if (allowed.count(sid))
{
stale.push_back(sid);
@@ -753,11 +784,57 @@ void RevokeBlockedSpellsForPlayer(Player* pl)
return false;
}
// Allowlist for ACTIVE spells we explicitly want kept as
// panel_spell_children, even though the general policy is "actives in
// children = legacy garbage, drop them" (see
// PruneSkillLineCascadeChildrenFromDb).
//
// The original kAttached set was 100% passives (Frost Fever, Blood
// Plague, Forceful Deflection, Runic Focus). For those, "passive ==
// keep" was a perfect proxy. Runeforging changed that: the 8 basic
// rune-enchant spells (53344, 53343, 53341, 53331, 53342, 53323,
// 54447, 54446) are ACTIVE casts that we DO want to attach to the
// Runeforging panel purchase so:
// * The Lua-substitute Runeforge UI can cast them (HasActiveSpell).
// * Refunding Runeforging cleans them up via the standard
// panel_spell_children unlearn path.
//
// Without this allowlist, PruneSkillLineCascadeChildrenFromDb runs
// immediately after PanelLearnSpellChain attaches them, sees them as
// non-passive, drops them, and inserts panel_spell_revoked rows --
// stranding the player with no usable runeforging menu.
//
// Every entry here MUST also appear in PanelLearnSpellChain::kAttached
// AND in OnPlayerLogin's kFixup list (or a shared source if those ever
// get factored out). The pair ordering is (parentHead, attachedSpell),
// matching kAttached / kFixup.
struct IntentionalActiveAttached { uint32 parent; uint32 child; };
static IntentionalActiveAttached const kIntentionalActiveAttached[] = {
{ 53428, 53344 }, // Runeforging -> Rune of the Fallen Crusader
{ 53428, 53343 }, // Runeforging -> Rune of Razorice
{ 53428, 53341 }, // Runeforging -> Rune of Cinderglacier
{ 53428, 53331 }, // Runeforging -> Rune of Lichbane
{ 53428, 53342 }, // Runeforging -> Rune of Spellshattering
{ 53428, 53323 }, // Runeforging -> Rune of Swordshattering
{ 53428, 54447 }, // Runeforging -> Rune of Spellbreaking
{ 53428, 54446 }, // Runeforging -> Rune of Swordbreaking
};
[[nodiscard]] static bool IsIntentionalActiveAttachedChild(uint32 parent, uint32 child)
{
for (auto const& e : kIntentionalActiveAttached)
if (e.parent == parent && e.child == child)
return true;
return false;
}
// Current policy: cascade-granted passives stick as panel_spell_children;
// only actives get revoked. This pass exists to scrub *legacy* rows that
// older logic inserted incorrectly — specifically, any active spell that
// ended up in panel_spell_children from a build that classified things
// differently. Passive children are always retained.
// differently. Passive children are always retained, as are entries
// whitelisted via kIntentionalActiveAttached (Runeforging rune-enchants
// are active casts that we deliberately attach as children).
static void PruneSkillLineCascadeChildrenFromDb(Player* pl, uint32 lowGuid)
{
if (!pl)
@@ -776,6 +853,8 @@ static void PruneSkillLineCascadeChildrenFromDb(Player* pl, uint32 lowGuid)
SpellInfo const* info = sSpellMgr->GetSpellInfo(child);
if (info && info->IsPassive())
continue; // passives always stay
if (IsIntentionalActiveAttachedChild(parent, child))
continue; // intentional active attachment
// Active in children -> legacy garbage. Drop the row, revoke the
// spell, and persist into panel_spell_revoked so the login sweep
// catches future cascade re-fires.
@@ -987,6 +1066,89 @@ void CollectSpellChainIds(uint32 baseSpellId, std::unordered_set<uint32>& out)
}
}
// Riding-skill gating for spells whose effective rank in the spellbook
// depends on the player's flying skill (currently the Druid forms
// Flight Form 33943 / Swift Flight Form 40120). Returns true when the
// player is allowed to learn this specific spell id at this moment.
//
// 33943 (Flight Form, 150% flight) requires 34090 Expert Riding.
// 40120 (Swift Flight Form, 280% flight) requires 34091 Artisan Riding.
//
// Any other spell id is always allowed (returns true). Used by
// `PanelLearnSpellChain` so a Paragon panel purchase / level-up cascade
// silently skips the unaffordable rank but keeps walking the chain --
// e.g. a player with Expert Riding only gets Flight Form, never Swift,
// even though both ranks are in the same SpellChain.dbc graph.
[[nodiscard]] bool IsParagonSpellAllowedByRidingSkill(Player* pl, uint32 spellId)
{
if (!pl)
return true;
if (spellId == 33943)
return pl->HasSpell(34090) || pl->HasSpell(34091); // expert OR artisan
if (spellId == 40120)
return pl->HasSpell(34091); // artisan required for swift
return true;
}
// Walk a rank chain and learn every rank up to the player's current
// level (and not past riding-skill gates), without any of the
// PanelLearnSpellChain panel/AE bookkeeping. Used by talent-grant
// cascades (Mangle / Feral Charge / Mutilate / etc.) where the talent
// LEARN_SPELL effect grants the rank-1 ability and stock would have
// upgraded it via Player::learnSkillRewardedSpells -- but the Paragon
// class-skill cascade is intentionally disabled (Player.cpp guard), so
// nothing else picks up the higher ranks. Idempotent: skips ranks the
// player already has, so safe to re-run on level-up / login.
void TeachLevelGatedAbilityChainNoPanel(Player* pl, uint32 chainHead)
{
if (!pl || !chainHead)
return;
uint32 const playerLevel = pl->GetLevel();
uint32 const firstId = sSpellMgr->GetFirstSpellInChain(chainHead);
uint32 cur = firstId ? firstId : chainHead;
while (cur)
{
SpellInfo const* info = sSpellMgr->GetSpellInfo(cur);
if (!info)
break;
uint32 const reqLv = GetParagonPanelSpellRequiredLevel(info);
if (playerLevel < reqLv)
break;
if (!pl->HasSpell(cur) && IsParagonSpellAllowedByRidingSkill(pl, cur))
pl->learnSpell(cur, false);
uint32 const next = sSpellMgr->GetNextSpellInChain(cur);
if (!next || next == cur)
break;
cur = next;
}
}
// Walk every SPELL_EFFECT_LEARN_SPELL on `talentRankSpellId` (the
// `TalentEntry::RankID[r]` of a talent rank) and, for each granted
// spell, run TeachLevelGatedAbilityChainNoPanel so the player ends up
// with the highest rank their level can support. Mangle, Feral Charge,
// Mutilate, etc. all fit this pattern.
void CascadeRanksForTalentLearnSpellEffects(Player* pl, uint32 talentRankSpellId)
{
if (!pl || !talentRankSpellId)
return;
SpellInfo const* rankInfo = sSpellMgr->GetSpellInfo(talentRankSpellId);
if (!rankInfo)
return;
for (uint8 e = 0; e < MAX_SPELL_EFFECTS; ++e)
{
if (rankInfo->Effects[e].Effect != SPELL_EFFECT_LEARN_SPELL)
continue;
uint32 const grantId = rankInfo->Effects[e].TriggerSpell;
if (!grantId)
continue;
TeachLevelGatedAbilityChainNoPanel(pl, grantId);
}
}
// True when `depSpellId` (any rank in its chain) is the target of a
// SPELL_EFFECT_LEARN_SPELL on any rank of the anchor purchase chain
// (`anchorChainHead` is the chain head / panel_spells.spell_id).
@@ -1103,7 +1265,7 @@ void PanelLearnSpellChain(Player* pl, uint32 baseSpellId)
if (playerLevel < reqLv)
break;
if (!pl->HasSpell(cur))
if (!pl->HasSpell(cur) && IsParagonSpellAllowedByRidingSkill(pl, cur))
{
std::unordered_set<uint32> before = SnapshotKnownSpells(pl);
if (diag)
@@ -1145,13 +1307,45 @@ void PanelLearnSpellChain(Player* pl, uint32 baseSpellId)
if (!dep)
continue;
if (dep->IsPassive())
// Meta-skill cascade carve-out. Runeforging (776) is a
// CLASS-category skill that, once granted, is supposed to
// cascade ALL its rune-enchant spells (Rune of the Fallen
// Crusader, Razorice, Cinderglacier, Lichbane, Spell-/
// Sword-shattering, Spell-/Sword-breaking, Stoneskin
// Gargoyle, Nerubian Carapace) for the player to choose
// from at a runeforge anvil. Those rune-enchants are
// ACTIVE spells, so the default policy below would
// revoke them and the player would learn Runeforging
// for nothing. Treat the whole cluster the same way we
// treat passive deps: persist as children of the panel
// purchase so refund/unlearn drops them too, but do NOT
// revoke them.
//
// Detection: walk the dep's own SkillLineAbility entries
// and check for SKILL_RUNEFORGING. This auto-handles all
// 10 rune-enchant spells without an ID-by-ID allowlist.
bool isMetaSkillCascade = false;
{
auto bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spellId);
for (auto it = bounds.first; it != bounds.second; ++it)
{
if (it->second->SkillLine == SKILL_RUNEFORGING)
{
isMetaSkillCascade = true;
break;
}
}
}
if (dep->IsPassive() || isMetaSkillCascade)
{
DbInsertPanelSpellChild(lowGuid, trackId, spellId);
if (diag)
LOG_INFO("module",
"[paragon-diag] +{} (passive dep, kept as child of {})",
spellId, trackId);
"[paragon-diag] +{} ({} dep, kept as child of {})",
spellId,
isMetaSkillCascade ? "meta-skill" : "passive",
trackId);
}
else
{
@@ -1218,6 +1412,34 @@ void PanelLearnSpellChain(Player* pl, uint32 baseSpellId)
{ 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)
// Runeforging -> 8 basic rune-enchants. The
// SkillLineAbility rows for these (skill 776) all ship
// with AcquireMethod = 0 in the DBC (i.e. NOT auto-learn-
// on-skill-grant). For stock DKs the engine's hardcoded
// runeforging UI hand-rolls the cast for whichever rune
// the player picks, but for our Lua-substitute UI the
// server's HandleCastSpellOpcode / HasActiveSpell gate
// rejects the cast unless the spell is in the spellbook.
// Force-attach them as panel children so:
// 1. The player actually owns the spells (cast works).
// 2. Refunding Runeforging cleans them up via the
// standard panel_spell_children unlearn path.
// The two ADVANCED runes (Stoneskin Gargoyle 62158 and
// Nerubian Carapace 70164) are intentionally NOT listed:
// retail gates them behind item drops from heroic
// dungeons / Naxx / ICC, and our SkillLineAbility rows
// for them already use AcquireMethod=0 so the player
// gets them when they pick up the appropriate item, not
// for free with Runeforging itself.
{ 53428, 53344 }, // Runeforging -> Rune of the Fallen Crusader
{ 53428, 53343 }, // Runeforging -> Rune of Razorice
{ 53428, 53341 }, // Runeforging -> Rune of Cinderglacier
{ 53428, 53331 }, // Runeforging -> Rune of Lichbane
{ 53428, 53342 }, // Runeforging -> Rune of Spellshattering
{ 53428, 53323 }, // Runeforging -> Rune of Swordshattering
{ 53428, 54447 }, // Runeforging -> Rune of Spellbreaking
{ 53428, 54446 }, // Runeforging -> Rune of Swordbreaking
};
// Self-heal: a previous build of mod-paragon (briefly shipped)
@@ -2085,7 +2307,23 @@ bool HandleCommit(Player* pl, std::string const& body, std::string* err)
uint32 const targetRank = std::min<uint32>(currentRank + delta, MAX_TALENT_RANK);
for (uint32 r = currentRank; r < targetRank; ++r)
{
pl->LearnTalent(tid, r, /*command=*/true);
// Fractured / Paragon: talents that LEARN_SPELL an ability
// (Mangle, Feral Charge, Mutilate, ...) only directly grant the
// rank-1 ability spell. Stock classes auto-rank-up the granted
// spell via Player::learnSkillRewardedSpells on level-up, but
// that path is intentionally disabled for Paragon class skill
// lines (Player.cpp guard) -- so without this cascade the
// ability stays at rank 1 forever (Mangle Bear 33878 instead
// of 33986 / 33987 / 48563 / 48564). Walk every LEARN_SPELL
// target on this rank's RankID spell and grant the highest
// rank the player's level allows.
if (TalentEntry const* freshTe = sTalentStore.LookupEntry(tid))
if (uint32 rankSpell = freshTe->RankID[r])
CascadeRanksForTalentLearnSpellEffects(pl, rankSpell);
}
}
for (auto const& [tid, delta] : talentDeltas)
@@ -2157,9 +2395,21 @@ void PushSpellSnapshot(Player* pl)
continue;
}
SpellInfo const* info = sSpellMgr->GetSpellInfo(sid);
if (info && info->HasAttribute(SPELL_ATTR0_DO_NOT_DISPLAY))
continue;
// Note: we deliberately do NOT filter SPELL_ATTR0_DO_NOT_DISPLAY
// here. Earlier builds did, on the theory that hidden spells
// shouldn't appear in the spellbook-style Overview tab. That
// turned out to be wrong: cascade-granted hidden passives
// (Forceful Deflection, Frost Fever, ...) live in
// panel_spell_children, not in panel_spells -- so the only
// entries that ever land in this query are the chain heads
// the player explicitly purchased. Those MUST appear in the
// Overview even if their DBC entry is hidden, because they
// are the player's actual purchases (e.g. Runeforging 53428
// is hidden in the DBC but is the entire Runeforging panel
// purchase). Filtering them out left chars whose only buy
// was Runeforging with an empty Overview tab -- looked like
// a regression but was actually the existing snapshot logic
// mismatching the panel's user-facing semantics.
std::string token = (first ? "" : ",") + std::to_string(sid);
if (buf.size() + token.size() > kSnapshotChunkBudget)
@@ -3919,6 +4169,24 @@ public:
{ 45477, 59921 }, // Icy Touch -> Frost Fever (passive)
{ 45477, 61455 }, // Icy Touch -> Runic Focus
{ 45902, 49410 }, // Blood Strike -> Forceful Deflection
// Runeforging -> 8 basic rune-enchants. Mirror of
// PanelLearnSpellChain::kAttached: the SLA rows for
// these (skill 776) ship with AcquireMethod=0 so the
// engine's normal cascade never grants them, and for
// the substitute Lua runeforging UI to actually be
// able to cast them HasActiveSpell needs to return
// true. Existing Paragon characters that bought
// Runeforging before this fix landed get them
// retro-granted on their next login.
{ 53428, 53344 }, // Runeforging -> Fallen Crusader
{ 53428, 53343 }, // Runeforging -> Razorice
{ 53428, 53341 }, // Runeforging -> Cinderglacier
{ 53428, 53331 }, // Runeforging -> Lichbane
{ 53428, 53342 }, // Runeforging -> Spellshattering
{ 53428, 53323 }, // Runeforging -> Swordshattering
{ 53428, 54447 }, // Runeforging -> Spellbreaking
{ 53428, 54446 }, // Runeforging -> Swordbreaking
};
for (auto const& lf : kFixup)
{
@@ -4013,6 +4281,45 @@ public:
ReconcileEssenceForPlayer(player);
SaveCurrencyToDb(player);
PushCurrency(player);
// Fractured / Paragon: rank-up cascade for level-up. Without this,
// higher ranks of panel-purchased spells AND talent-LEARN_SPELL
// granted abilities (Mangle, Feral Charge, Mutilate, ...) never
// appear on ding because Player::learnSkillRewardedSpells is
// disabled for the class skill line on Paragon (intentional, to
// keep the panel as the sole authority over class abilities).
//
// Cheap re-walks: PanelLearnSpellChain / TeachLevelGated... both
// skip ranks the player already has, so the only real work each
// level-up is adding the one new rank the player just qualified
// for (if any).
if (QueryResult r = CharacterDatabase.Query(
"SELECT spell_id FROM character_paragon_panel_spells WHERE guid = {}", lowGuid))
{
do
{
uint32 const head = r->Fetch()[0].Get<uint32>();
PanelLearnSpellChain(player, head);
} while (r->NextRow());
}
if (QueryResult tr = CharacterDatabase.Query(
"SELECT talent_id, `rank` FROM character_paragon_panel_talents WHERE guid = {}", lowGuid))
{
do
{
Field* f = tr->Fetch();
uint32 const talentId = f[0].Get<uint32>();
uint32 const rank = f[1].Get<uint32>();
TalentEntry const* te = sTalentStore.LookupEntry(talentId);
if (!te)
continue;
uint32 const cap = std::min<uint32>(rank, MAX_TALENT_RANK);
for (uint32 i = 0; i < cap; ++i)
if (uint32 rankSpell = te->RankID[i])
CascadeRanksForTalentLearnSpellEffects(player, rankSpell);
} while (tr->NextRow());
}
}
bool OnPlayerCanLearnTalent(Player* player, TalentEntry const* talent, uint32 /*rank*/) override
+8 -5
View File
@@ -7,13 +7,15 @@
# AZEROTH_BIN=/path/to/azeroth-server/bin bash scripts/start-azeroth-servers.sh
#
# Environment:
# AZEROTH_BIN — directory with authserver and worldserver (default: /root/azeroth-server/bin)
# AZEROTH_BIN — directory with authserver and worldserver (default: /home/fractured-panel/azeroth-server/bin)
# AZEROTH_LOG_DIR — log directory (default: <parent of bin>/logs)
set -euo pipefail
BIN_DIR="${AZEROTH_BIN:-/root/azeroth-server/bin}"
LOG_DIR="${AZEROTH_LOG_DIR:-$(cd "$(dirname "$BIN_DIR")" && pwd)/logs}"
BIN_DIR="${AZEROTH_BIN:-/home/fractured-panel/azeroth-server/bin}"
BASE_DIR="$(cd "$(dirname "$BIN_DIR")" && pwd)"
LOG_DIR="${AZEROTH_LOG_DIR:-${BASE_DIR}/logs}"
CONF_DIR="${BASE_DIR}/etc"
AUTH_BIN="${BIN_DIR}/authserver"
WORLD_BIN="${BIN_DIR}/worldserver"
@@ -35,15 +37,16 @@ mkdir -p "$LOG_DIR"
cd "$BIN_DIR"
nohup "$AUTH_BIN" >>"$LOG_DIR/authserver.log" 2>&1 &
nohup "$AUTH_BIN" -c "${CONF_DIR}/authserver.conf" >>"$LOG_DIR/authserver.log" 2>&1 &
disown || true
sleep 2
nohup "$WORLD_BIN" >>"$LOG_DIR/worldserver.log" 2>&1 &
nohup "$WORLD_BIN" -c "${CONF_DIR}/worldserver.conf" >>"$LOG_DIR/worldserver.log" 2>&1 &
disown || true
echo "Started authserver and worldserver (survives SSH disconnect)."
echo "Bin: $BIN_DIR"
echo "Config: $CONF_DIR"
echo "Logs: $LOG_DIR/authserver.log"
echo " $LOG_DIR/worldserver.log"
+21
View File
@@ -35,6 +35,7 @@ FULL_BUILD=0
COMPILE_ONLY=0
DRY_RUN=0
DO_RUN_AFTER=0
INSTALL_PREFIX=""
POST_UPDATE_CMD="${FRACTURED_POST_UPDATE_CMD:-}"
GIT_REMOTE="${FRACTURED_GIT_REMOTE:-origin}"
@@ -49,6 +50,7 @@ Options:
--no-pull Skip git pull (only compile current tree).
--full ./acore.sh compiler all (clean + configure + compile).
--compile-only ./acore.sh compiler compile (incremental).
--prefix PATH Override CMAKE_INSTALL_PREFIX (updates conf/config.sh BINPATH).
--dry-run Print commands without running them.
--run-after [CMD] Run shell command after successful compile. If CMD is omitted,
uses FRACTURED_POST_UPDATE_CMD from the environment.
@@ -87,6 +89,15 @@ while [[ $# -gt 0 ]]; do
COMPILE_ONLY=1
shift
;;
--prefix)
shift
if [[ $# -eq 0 || "$1" == -* ]]; then
echo "error: --prefix requires a path argument" >&2
exit 2
fi
INSTALL_PREFIX="$1"
shift
;;
--dry-run)
DRY_RUN=1
shift
@@ -129,6 +140,16 @@ fi
cd "$ROOT"
if [[ -n "$INSTALL_PREFIX" ]]; then
echo "==> updating conf/config.sh BINPATH to: $INSTALL_PREFIX"
if grep -q '^BINPATH=' conf/config.sh; then
run sed -i "s|^BINPATH=.*|BINPATH=\"$INSTALL_PREFIX\"|" conf/config.sh
else
echo "BINPATH=\"$INSTALL_PREFIX\"" >> conf/config.sh
fi
export BINPATH="$INSTALL_PREFIX"
fi
if [[ "$DO_RUN_AFTER" -eq 1 && -z "${POST_UPDATE_CMD// }" ]]; then
echo "error: --run-after needs a command or FRACTURED_POST_UPDATE_CMD set in the environment." >&2
exit 2
+37 -18
View File
@@ -7143,14 +7143,16 @@ bool Player::CheckAttackFitToAuraRequirement(WeaponAttackType attackType, AuraEf
if (spellInfo->EquippedItemClass == -1)
return true;
// Fractured / Paragon: cross-class wildcard relaxes weapon-class subclass
// gates on per-swing proc matches. A Paragon's talent list spans every
// class so a stock weapon-subclass mask (e.g. Maelstrom Weapon's
// axe/mace/staff/fist/dagger restriction) excludes weapons the player
// can legitimately wield. Accept any equipped weapon in attackType slot
// when listener is a Paragon AND the spell gates on ITEM_CLASS_WEAPON;
// ITEM_CLASS_ARMOR (shield) gates still enforce the original mask.
if (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON && IsParagonWildcardCaller(this))
// Fractured / Paragon: cross-class wildcard relaxes the weapon-subclass
// gate ONLY for the curated allowlist of cross-class proc talents
// (currently just Maelstrom Weapon 51528-51532). Weapon-specialization
// talents like Sword Specialization, Mace Specialization, Hack and
// Slash, Two-Handed Weapon Specialization etc. deliberately stay
// weapon-gated for Paragon -- the player picks a weapon and the
// matching specialization passive activates, same as any class.
if (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON
&& IsParagonWildcardCaller(this)
&& IsParagonWeaponSubclassWildcardSpell(spellInfo->Id))
return GetWeaponForAttack(attackType, true) != nullptr;
Item* item = GetWeaponForAttack(attackType, true);
@@ -12046,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)
@@ -12591,15 +12606,19 @@ bool Player::HasItemFitToSpellRequirements(SpellInfo const* spellInfo, Item cons
if (spellInfo->EquippedItemClass < 0)
return true;
// Fractured / Paragon: cross-class wildcard relaxes weapon-class subclass
// gates so passive talent auras (e.g. Maelstrom Weapon talents 51528-51532)
// attach for any equipped weapon, not just the talent's restrictive
// subclass mask. Mirrors CheckAttackFitToAuraRequirement so per-swing
// proc match agrees with talent-attach time. Still requires *some* weapon
// to be equipped (otherwise unarmed Paragons would auto-activate every
// weapon-gated talent in the game). ITEM_CLASS_ARMOR (shield) is left
// alone -- shield-gated talents still need an actual shield.
if (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON && IsParagonWildcardCaller(this))
// Fractured / Paragon: cross-class wildcard relaxes the weapon-subclass
// gate ONLY for the curated allowlist of cross-class proc talents
// (currently just Maelstrom Weapon 51528-51532) so the passive talent
// aura attaches when the player wields a non-stock weapon. Weapon-
// specialization talents (Sword/Mace Specialization, Hack and Slash,
// Two-Handed Weapon Specialization, etc.) deliberately stay weapon-
// gated -- they're meant to bind to a specific weapon type. Still
// requires *some* weapon equipped (unarmed Paragons don't auto-activate
// weapon talents). ITEM_CLASS_ARMOR (shield) is left alone -- shield-
// gated talents still need an actual shield.
if (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON
&& IsParagonWildcardCaller(this)
&& IsParagonWeaponSubclassWildcardSpell(spellInfo->Id))
{
for (uint8 i = EQUIPMENT_SLOT_MAINHAND; i < EQUIPMENT_SLOT_TABARD; ++i)
if (Item const* item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, i))
+19 -1
View File
@@ -474,7 +474,25 @@ void Player::UpdateAttackPowerAndDamage(bool ranged)
switch (GetShapeshiftForm())
{
case FORM_CAT:
val2 = (GetLevel() * mLevelMult) + GetStat(STAT_STRENGTH) * 2.0f + GetStat(STAT_AGILITY) - 20.0f + weapon_bonus + m_baseFeralAP;
// Fractured: Cat Form gets 2 AP per Agility instead of stock 1.
// Field reports said "weapons dont automatically feature feral
// AP on this server and nothing is currently rescaled, super
// low feral scale" -- specifically a CAT issue, not a bear
// issue (the resident bear had 11k AP, the resident cat was
// miles behind because Stam > AP and Armor > AP for bears
// hides the missing weapon-AP for them but cat's whole
// mainline is melee crits scaling off AP). The cleanest knob
// that does NOT touch bear is the AGI multiplier in this
// switch -- bears get STR*2 with no AGI term, so doubling
// the AGI coefficient lifts cat's primary scaling stat
// without re-buffing bear. Also pairs with the cat-form
// Master Shapeshifter buff in SpellAuraEffects.cpp's
// FORM_CAT branch (bp doubled there). Together that lands
// the resident Feral expert's recommendation
// ("instead of adding a new passive, you could probably
// just increase Cat Form's Master Shapeshifter value along
// with its tooltip, alongside buffing the agi scaling").
val2 = (GetLevel() * mLevelMult) + GetStat(STAT_STRENGTH) * 2.0f + GetStat(STAT_AGILITY) * 2.0f - 20.0f + weapon_bonus + m_baseFeralAP;
break;
case FORM_BEAR:
case FORM_DIREBEAR:
+116
View File
@@ -103,6 +103,122 @@ bool ParagonFamilyMatches(Unit const* listener, uint32 expectedFamily, uint32 ac
return expectedFamily == actualFamily;
}
bool IsParagonWeaponSubclassWildcardSpell(uint32 spellId)
{
if (!spellId)
return false;
// Resolve to the first-rank id so callers can pass any rank.
uint32 firstRankId = spellId;
if (SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId))
if (SpellInfo const* first = info->GetFirstRankSpell())
firstRankId = first->Id;
switch (firstRankId)
{
// Maelstrom Weapon (talent ranks 51528 / 51529 / 51530 / 51531 / 51532).
// Cross-class proc talent that should fire off any equipped weapon
// for a Paragon caster (1H sword, polearm, staff, fist, dagger, etc.).
case 51528:
return true;
default:
return false;
}
}
bool IsFracturedExclusiveStanceSpell(uint32 spellId)
{
if (!spellId)
return false;
// Resolve to the first-rank id so callers can pass any rank. This means
// every rank of Aspect of the Hawk / Wild / Pack / Dragonhawk is covered
// by listing only the rank-1 id below; same for druid forms that have
// multiple ranks via talent (none in WotLK actually, but kept consistent).
uint32 firstRankId = spellId;
if (SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId))
if (SpellInfo const* first = info->GetFirstRankSpell())
firstRankId = first->Id;
switch (firstRankId)
{
// -- Warrior stances (engine-shapeshifts; engine already mutually
// excludes them with each other and with druid forms via
// AuraEffect::HandleAuraModShapeshift's RemoveAurasByType, but we
// list them here so they participate in the union with presences /
// aspects).
case 2457: // Battle Stance
case 71: // Defensive Stance
case 2458: // Berserker Stance
// -- Paragon Advancement warrior stance clones (951010-951012).
case 951010:
case 951011:
case 951012:
// -- Druid combat forms (engine-shapeshifts).
case 5487: // Bear Form
case 9634: // Dire Bear Form
case 768: // Cat Form
case 24858: // Moonkin Form
case 33891: // Tree of Life Form
// -- Druid utility forms (engine-shapeshifts; included per design
// decision 2026-05-11 -- player must drop Travel/Aquatic/Flight to
// apply Hawk / Frost Presence / Berserker Stance, and vice versa).
case 783: // Travel Form
case 1066: // Aquatic Form
case 33943: // Flight Form
case 40120: // Swift Flight Form
// -- Shaman utility form (engine-shapeshift FORM_GHOSTWOLF).
case 2645: // Ghost Wolf
// -- Rogue base stealth (engine-shapeshift FORM_STEALTH). Shadow
// Dance (51713) is intentionally NOT listed -- it is a 6s
// stealth-burst on a 60s CD, gating it would defeat its purpose.
case 1784: // Stealth
// -- Priest combat form (engine-shapeshift FORM_SHADOW).
case 15473: // Shadowform
// -- Warlock combat form (engine-shapeshift FORM_METAMORPHOSIS).
case 47241: // Metamorphosis
// -- Death Knight Presences. NOT engine-shapeshifts in stock AC --
// they are regular auras that the client just renders in the
// stance bar -- which is exactly why stock DK can stack them on
// top of Bear Form / Defensive Stance / Aspect of the Hawk on a
// Paragon character. Listing them here is what plugs the gap.
case 48266: // Blood Presence
case 48263: // Frost Presence
case 48265: // Unholy Presence
// -- Paragon Advancement DK presence clones (951013-951015).
case 951013:
case 951014:
case 951015:
// -- Hunter Aspects (combat). Like presences, these are regular
// auras stock AC, not engine-shapeshifts; rank-1 ids cover all
// ranks via GetFirstRankSpell. Cheetah / Pack are the utility
// aspects -- included per design decision so a hunter must pick
// between Hawk and Cheetah (no more "always Hawk while running",
// matches Ascension's nerf rationale for Monkey).
case 13165: // Aspect of the Hawk (rank 1; ranks 14318/14319/14320/14321/14322/25296/27044 covered via first-rank)
case 5118: // Aspect of the Cheetah
case 13159: // Aspect of the Pack (rank 1; rank 27047 covered via first-rank)
case 20043: // Aspect of the Wild (rank 1; ranks 20190/27045 covered via first-rank)
case 13161: // Aspect of the Beast
case 13163: // Aspect of the Monkey
case 34074: // Aspect of the Viper
case 61846: // Aspect of the Dragonhawk (rank 1; rank 61847 covered via first-rank)
return true;
default:
return false;
}
}
float baseMoveSpeed[MAX_MOVE_TYPE] =
{
2.5f, // MOVE_WALK
+41
View File
@@ -2287,6 +2287,47 @@ private:
// beyond what they already include via Unit.h's transitive headers.
[[nodiscard]] bool ParagonFamilyMatches(Unit const* listener, uint32 expectedFamily, uint32 actualFamily);
// Fractured / Paragon: returns true iff `spellId` is on the curated allowlist
// of cross-class proc talents whose `EquippedItemSubClassMask` should be
// bypassed for Paragon casters (e.g. Maelstrom Weapon ranks 51528-51532).
// Used by `Player::HasItemFitToSpellRequirements`,
// `Player::CheckAttackFitToAuraRequirement`, and
// `Aura::IsProcTriggeredOnEvent` to let these specific talents fire from
// any equipped weapon while keeping weapon-specialization talents
// (Sword Specialization, Mace Specialization, Hack and Slash, Two-Handed
// Weapon Specialization, etc.) gated by their stock weapon-class mask.
// The allowlist is matched against `SpellInfo::GetFirstRankSpell()`'s id
// so all ranks of a talent are covered by listing the rank-1 id.
[[nodiscard]] bool IsParagonWeaponSubclassWildcardSpell(uint32 spellId);
// Fractured: returns true iff `spellId` is one of the cross-class
// "stance-like" auras that we treat as mutually exclusive on this server,
// regardless of the caster's class. Stock AC engine-shapeshifts (warrior
// stances, druid forms, Shadowform, Metamorphosis) already auto-replace
// each other via `RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT)` in
// `AuraEffect::HandleAuraModShapeshift`, but DK Presences and Hunter
// Aspects are regular auras (the client just renders them in the stance
// bar), so they coexist with shapeshifts in stock AC. The Fractured rule
// makes the entire union mutually exclusive: warrior stances + druid
// forms (combat AND utility -- Travel/Aquatic/Flight/Swift Flight) +
// Ghost Wolf + Stealth + Shadowform + Metamorphosis + DK Presences +
// Hunter Aspects (combat AND utility -- Cheetah/Pack). Casting any of
// them removes any other from the same set, so e.g. a Paragon DK can no
// longer stack Frost Presence on top of Bear Form, and a hunter must
// pick between Hawk and Cheetah even out of combat.
//
// The set is matched against `SpellInfo::GetFirstRankSpell()`'s id so
// every rank of every aspect / form is covered by listing the rank-1 id.
// Server-wide -- this is *not* gated on CLASS_PARAGON because the only
// stock-class-only effect of the rule (a DK losing Travel Form when
// they cast Frost Presence) is impossible: stock DKs cannot shapeshift.
// Used by `Aura::CanStackWith` to refuse stacking, which then drives
// `Unit::_RemoveNoStackAurasDueToAura` to drop the older aura -- the
// same mechanism Battle Elixirs / Curses already use (spell_group rule
// SPELL_GROUP_STACK_RULE_EXCLUSIVE), implemented in C++ here so we do
// not have to enumerate every rank of every aspect / form in SQL.
[[nodiscard]] bool IsFracturedExclusiveStanceSpell(uint32 spellId);
namespace Acore
{
// Binary predicate for sorting Units based on percent value of a power
+7 -1
View File
@@ -99,7 +99,13 @@ enum ShapeshiftForm
FORM_FLIGHT = 0x1D,
FORM_STEALTH = 0x1E,
FORM_MOONKIN = 0x1F,
FORM_SPIRITOFREDEMPTION = 0x20
FORM_SPIRITOFREDEMPTION = 0x20,
// Fractured / Paragon: Character Advancement warrior stance clones (Spell.dbc
// MOD_SHAPESHIFT -> SpellShapeshiftForm 33-35, BonusActionBar=0, no client bar swap).
FORM_PARAGON_BATTLE_STANCE = 33,
FORM_PARAGON_DEFENSIVE_STANCE = 34,
FORM_PARAGON_BERSERKER_STANCE = 35,
};
enum ShapeshiftFlags
@@ -1373,12 +1373,15 @@ void AuraEffect::HandleShapeshiftBoosts(Unit* target, bool apply) const
HotWSpellId = 24899;
break;
case FORM_BATTLESTANCE:
case FORM_PARAGON_BATTLE_STANCE:
spellId = 21156;
break;
case FORM_DEFENSIVESTANCE:
case FORM_PARAGON_DEFENSIVE_STANCE:
spellId = 7376;
break;
case FORM_BERSERKERSTANCE:
case FORM_PARAGON_BERSERKER_STANCE:
spellId = 7381;
break;
case FORM_MOONKIN:
@@ -1545,7 +1548,21 @@ void AuraEffect::HandleShapeshiftBoosts(Unit* target, bool apply) const
// Master Shapeshifter - Cat
if (AuraEffect const* aurEff = target->GetDummyAuraEffect(SPELLFAMILY_GENERIC, 2851, 0))
{
int32 bp = aurEff->GetAmount();
// Fractured: cat-only Master Shapeshifter bonus is
// doubled (rank 1: 2% -> 4%, rank 2: 4% -> 8%) to
// make Feral Cat builds feel less "super low feral
// scale" without touching bear / moonkin / tree (the
// FORM_BEAR / FORM_MOONKIN / FORM_TREE branches
// below pass `bp` straight through, unchanged). The
// talent's own SpellInfo Effects[].BasePoints is
// intentionally NOT bumped -- aurEff->GetAmount()
// returns the per-rank talent value (2 / 4) shared
// across all four forms, so we apply the cat
// multiplier here at the cast site, leaving every
// other form on the stock value. Pairs with the
// Cat-Form AGI doubling in StatSystem.cpp's
// UpdateAttackPowerAndDamage FORM_CAT branch.
int32 bp = aurEff->GetAmount() * 2;
target->CastCustomSpell(target, 48420, &bp, nullptr, nullptr, true);
}
break;
@@ -1981,7 +1998,7 @@ void AuraEffect::HandlePhase(AuraApplication const* aurApp, uint8 mode, bool app
/*** UNIT MODEL ***/
/**********************/
void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mode, bool apply) const
static void Fractured_ApplyShapeshiftFormFromAuraEffect(AuraEffect const* aurEff, AuraApplication const* aurApp, uint8 mode, bool apply, ShapeshiftForm form)
{
if (!(mode & AURA_EFFECT_HANDLE_REAL_OR_REAPPLY_MASK))
return;
@@ -1990,8 +2007,6 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
uint32 modelid = 0;
Powers PowerType = POWER_MANA;
ShapeshiftForm form = ShapeshiftForm(GetMiscValue());
switch (form)
{
case FORM_CAT: // 0x01
@@ -2005,6 +2020,9 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
case FORM_BATTLESTANCE: // 0x11
case FORM_DEFENSIVESTANCE: // 0x12
case FORM_BERSERKERSTANCE: // 0x13
case FORM_PARAGON_BATTLE_STANCE:
case FORM_PARAGON_DEFENSIVE_STANCE:
case FORM_PARAGON_BERSERKER_STANCE:
PowerType = POWER_RAGE;
break;
@@ -2035,10 +2053,10 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
case FORM_SPIRITOFREDEMPTION: // 0x20
break;
default:
LOG_ERROR("spells.aura.effect", "Auras: Unknown Shapeshift Type: {}", GetMiscValue());
LOG_ERROR("spells.aura.effect", "Auras: Unknown Shapeshift Type: {}", aurEff->GetMiscValue());
}
modelid = target->GetModelForForm(form, GetId());
modelid = target->GetModelForForm(form, aurEff->GetId());
if (apply)
{
@@ -2073,8 +2091,8 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
// remove other shapeshift before applying a new one
// xinef: rogue shouldnt be wrapped by this check (shadow dance vs stealth)
if (GetSpellInfo()->SpellFamilyName != SPELLFAMILY_ROGUE)
target->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT, ObjectGuid::Empty, GetBase());
if (aurEff->GetSpellInfo()->SpellFamilyName != SPELLFAMILY_ROGUE)
target->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT, ObjectGuid::Empty, aurEff->GetBase());
// stop handling the effect if it was removed by linked event
if (aurApp->GetRemoveMode())
@@ -2098,13 +2116,13 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
if (AuraEffect const* dummy = target->GetDummyAuraEffect(SPELLFAMILY_DRUID, 238, 0))
FurorChance = std::max(dummy->GetAmount(), 0);
switch (GetMiscValue())
switch (aurEff->GetMiscValue())
{
case FORM_CAT:
{
int32 basePoints = int32(std::min(oldPower, FurorChance));
target->SetPower(POWER_ENERGY, 0);
target->CastCustomSpell(target, 17099, &basePoints, nullptr, nullptr, true, nullptr, this);
target->CastCustomSpell(target, 17099, &basePoints, nullptr, nullptr, true, nullptr, aurEff);
break;
}
case FORM_BEAR:
@@ -2178,13 +2196,16 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
case FORM_BATTLESTANCE:
case FORM_DEFENSIVESTANCE:
case FORM_BERSERKERSTANCE:
case FORM_PARAGON_BATTLE_STANCE:
case FORM_PARAGON_DEFENSIVE_STANCE:
case FORM_PARAGON_BERSERKER_STANCE:
{
uint32 Rage_val = 0;
// Defensive Tactics
if (form == FORM_DEFENSIVESTANCE)
if (form == FORM_DEFENSIVESTANCE || form == FORM_PARAGON_DEFENSIVE_STANCE)
{
if (AuraEffect const* aurEff = target->IsScriptOverriden(m_spellInfo, 831))
Rage_val += aurEff->GetAmount() * 10;
if (AuraEffect const* scriptEff = target->IsScriptOverriden(aurEff->GetSpellInfo(), 831))
Rage_val += scriptEff->GetAmount() * 10;
}
// Stance mastery + Tactical mastery (both passive, and last have aura only in defense stance, but need apply at any stance switch)
if (target->IsPlayer())
@@ -2224,7 +2245,7 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
// adding/removing linked auras
// add/remove the shapeshift aura's boosts
HandleShapeshiftBoosts(target, apply);
aurEff->HandleShapeshiftBoosts(target, apply);
if (target->IsPlayer())
target->ToPlayer()->InitDataForForm();
@@ -2232,8 +2253,8 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
if (target->IsClass(CLASS_DRUID, CLASS_CONTEXT_ABILITY))
{
// Dash
if (AuraEffect* aurEff = target->GetAuraEffect(SPELL_AURA_MOD_INCREASE_SPEED, SPELLFAMILY_DRUID, 0, 0, 0x8))
aurEff->RecalculateAmount();
if (AuraEffect* dashSpeedEff = target->GetAuraEffect(SPELL_AURA_MOD_INCREASE_SPEED, SPELLFAMILY_DRUID, 0, 0, 0x8))
dashSpeedEff->RecalculateAmount();
// Disarm handling
// If druid shifts while being disarmed we need to deal with that since forms aren't affected by disarm
@@ -2267,6 +2288,11 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
if (target->IsPlayer())
{
SpellShapeshiftFormEntry const* shapeInfo = sSpellShapeshiftFormStore.LookupEntry(form);
if (!shapeInfo)
{
LOG_ERROR("spells.aura.effect", "Fractured_ApplyShapeshiftFormFromAuraEffect: missing SpellShapeshiftForm {}", uint32(form));
return;
}
// Learn spells for shapeshift form - no need to send action bars or add spells to spellbook
for (uint8 i = 0; i < MAX_SHAPESHIFT_SPELLS; ++i)
{
@@ -2280,6 +2306,11 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
}
}
void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mode, bool apply) const
{
Fractured_ApplyShapeshiftFormFromAuraEffect(this, aurApp, mode, apply, ShapeshiftForm(GetMiscValue()));
}
void AuraEffect::HandleAuraTransform(AuraApplication const* aurApp, uint8 mode, bool apply) const
{
if (!(mode & AURA_EFFECT_HANDLE_SEND_FOR_CLIENT_MASK))
@@ -5160,6 +5191,20 @@ void AuraEffect::HandleAuraDummy(AuraApplication const* aurApp, uint8 mode, bool
Unit* caster = GetCaster();
// Fractured: Paragon warrior stance clones (951010-951012) use SPELL_AURA_DUMMY on Spell.dbc **effect2**
// (misc = Paragon SpellShapeshiftForm 33-35). Effect1 is a separate DUMMY for the buff strip; passive stats
// (e.g. armor pen / threat) live on Effect3. **AttributesEx** clears SPELL_ATTR1_NO_AURA_ICON (DBC + SpellInfoCorrections)
// so the client shows an aura icon — stock Warrior stances keep that bit set on purpose.
if (GetAuraType() == SPELL_AURA_DUMMY && m_effIndex == 1)
{
uint32 const sid = GetSpellInfo()->Id;
if (sid == 951010 || sid == 951011 || sid == 951012)
{
Fractured_ApplyShapeshiftFormFromAuraEffect(this, aurApp, mode, apply, ShapeshiftForm(GetMiscValue()));
return;
}
}
if (mode & AURA_EFFECT_HANDLE_REAL)
{
// pet auras
+35 -13
View File
@@ -1973,6 +1973,31 @@ bool Aura::CanStackWith(Aura const* existingAura) const
|| (sameCaster && m_spellInfo->IsAuraExclusiveBySpecificPerCasterWith(existingSpellInfo)))
return false;
// Fractured: cross-class stance / form / presence / aspect exclusivity.
// Stock AC's engine-shapeshift removal in HandleAuraModShapeshift only
// covers warrior stances, druid forms, Shadowform, Metamorphosis, etc.
// -- DK Presences and Hunter Aspects are regular auras (the client just
// happens to render them in the stance bar) and therefore stack with
// engine-shapeshifts in stock AC. The Fractured rule (server-wide --
// see IsFracturedExclusiveStanceSpell in Unit.h for the curated set
// and the design rationale) treats the union of stances + forms (combat
// AND utility) + presences + aspects as mutually exclusive. Refusing
// to stack here triggers the same _RemoveNoStackAurasDueToAura cleanup
// path that Battle Elixirs / Curses already use, so the older aura
// drops and the newly-cast one applies cleanly. Different ranks of the
// same talent (e.g. Hawk rank 4 -> Hawk rank 7) are NOT treated as
// exclusive with each other -- IsFracturedExclusiveStanceSpell resolves
// to first-rank ids, so we compare those.
if (IsFracturedExclusiveStanceSpell(m_spellInfo->Id) && IsFracturedExclusiveStanceSpell(existingSpellInfo->Id))
{
SpellInfo const* newFirst = m_spellInfo->GetFirstRankSpell();
SpellInfo const* oldFirst = existingSpellInfo->GetFirstRankSpell();
uint32 newFirstId = newFirst ? newFirst->Id : m_spellInfo->Id;
uint32 oldFirstId = oldFirst ? oldFirst->Id : existingSpellInfo->Id;
if (newFirstId != oldFirstId)
return false;
}
// check spell group stack rules
switch (sSpellMgr->CheckSpellGroupStackRules(m_spellInfo, existingSpellInfo))
{
@@ -2255,20 +2280,17 @@ uint8 Aura::GetProcEffectMask(AuraApplication* aurApp, ProcEventInfo& eventInfo,
return 0;
if (!item->IsFitToSpellRequirements(GetSpellInfo()))
{
// Fractured / Paragon: cross-class wildcard relaxes weapon-
// class subclass gates on per-event proc evaluation. This
// mirrors Player::CheckAttackFitToAuraRequirement and
// Player::HasItemFitToSpellRequirements -- without this
// third bypass, the talent attaches (HasItemFit lets it),
// the per-swing match accepts the weapon (CheckAttackFit
// lets it), but IsProcTriggeredOnEvent still kills the
// proc here for any weapon outside the talent's stock
// subclass mask (e.g. Maelstrom Weapon on a Paragon
// wielding a 1H sword or polearm). Restricted to
// ITEM_CLASS_WEAPON so shield-gated talents still need
// an actual shield.
// Fractured / Paragon: cross-class wildcard relaxes the
// weapon-subclass gate ONLY for the curated allowlist of
// cross-class proc talents (currently just Maelstrom Weapon
// 51528-51532). Weapon-specialization talents (Sword/Mace
// Specialization, Hack and Slash, Two-Handed Weapon
// Specialization, etc.) deliberately stay weapon-gated for
// Paragon. Restricted to ITEM_CLASS_WEAPON so shield-gated
// talents still need an actual shield.
if (!(GetSpellInfo()->EquippedItemClass == ITEM_CLASS_WEAPON
&& IsParagonWildcardCaller(target)))
&& IsParagonWildcardCaller(target)
&& IsParagonWeaponSubclassWildcardSpell(GetSpellInfo()->Id)))
return 0;
}
}
+83 -45
View File
@@ -7732,56 +7732,64 @@ SpellCastResult Spell::CheckItems(uint32* param1, uint32* param2)
switch (pItem->GetTemplate()->SubClass)
{
case ITEM_SUBCLASS_WEAPON_THROWN:
{
uint32 ammo = pItem->GetEntry();
if (!m_caster->ToPlayer()->HasItemCount(ammo))
return SPELL_FAILED_NO_AMMO;
};
{
// Fractured: thrown abilities behave like DK runes -- they
// remain usable even when the player has run out of the
// throwing item. Stock AC returned SPELL_FAILED_NO_AMMO
// here; we just drop the gate. Spell::TakeAmmo's stack
// decrement is wrapped in a HasItemCount check via
// DestroyItemCount and will silently no-op at zero. The
// ranged-DPS bonus naturally vanishes when the stack runs
// out, so the player still throws but loses the per-shot
// damage contribution from the throwing item.
break;
};
case ITEM_SUBCLASS_WEAPON_GUN:
case ITEM_SUBCLASS_WEAPON_BOW:
case ITEM_SUBCLASS_WEAPON_CROSSBOW:
{
uint32 ammo = m_caster->ToPlayer()->GetUInt32Value(PLAYER_AMMO_ID);
if (!ammo)
{
// Requires No Ammo
if (m_caster->HasAura(46699))
break; // skip other checks
return SPELL_FAILED_NO_AMMO;
}
ItemTemplate const* ammoProto = sObjectMgr->GetItemTemplate(ammo);
if (!ammoProto)
return SPELL_FAILED_NO_AMMO;
if (ammoProto->Class != ITEM_CLASS_PROJECTILE)
return SPELL_FAILED_NO_AMMO;
// check ammo ws. weapon compatibility
switch (pItem->GetTemplate()->SubClass)
{
case ITEM_SUBCLASS_WEAPON_BOW:
case ITEM_SUBCLASS_WEAPON_CROSSBOW:
if (ammoProto->SubClass != ITEM_SUBCLASS_ARROW)
return SPELL_FAILED_NO_AMMO;
break;
case ITEM_SUBCLASS_WEAPON_GUN:
if (ammoProto->SubClass != ITEM_SUBCLASS_BULLET)
return SPELL_FAILED_NO_AMMO;
break;
default:
return SPELL_FAILED_NO_AMMO;
}
if (!m_caster->ToPlayer()->HasItemCount(ammo))
{
m_caster->ToPlayer()->SetUInt32Value(PLAYER_AMMO_ID, 0);
return SPELL_FAILED_NO_AMMO;
}
};
{
// Fractured: ranged abilities behave like DK runes -- they
// remain usable when the player has no ammo loaded or the
// quiver / pouch is empty. The DPS-bonus path (StatSystem.cpp:
// `weaponMin/MaxDamage += GetAmmoDPS() * attackSpeedMod`)
// reads `m_ammoDPS`, which is 0 when no ammo is loaded and
// recomputed via Player::_ApplyAmmoBonuses on equip / stack
// exhaustion, so a hunter with an empty bag still casts
// Steady Shot / Aimed Shot etc. -- they just lose the arrow
// / bullet DPS contribution.
//
// We deliberately do NOT clear PLAYER_AMMO_ID when the bag
// empties. Defense in depth alongside the data-side fix:
//
// * The primary client-side fix lives in Spell.dbc --
// SpellInfoCorrections.cpp's "drop EquippedItemClass on
// hunter shot abilities" block (mirrored client-side by
// fractured-tooling/_patch_spell_dbc_hunter_ammo.py)
// sets EquippedItemClass = -1 on every player-castable
// hunter shot, which removes the 3.3.5a client's
// "ranged weapon AND ammo slot non-empty" preflight
// gate entirely. After that, ammo is purely a
// server-side DPS bonus, never a hard requirement.
//
// * Keeping the (now-stale) ammo id in PLAYER_AMMO_ID
// field is harmless: TakeAmmo's DestroyItemCount
// silently no-ops when HasItemCount is 0, and
// _ApplyAmmoBonuses already recomputes m_ammoDPS to 0
// when the proto can no longer be found / the stack is
// empty. So the StatSystem.cpp ammo-DPS path gracefully
// degrades to "no bonus" the moment the bag goes empty.
//
// * Player un-equipping the ammo via the paper-doll
// right-click still routes through RemoveAmmo() and
// zeroes the field -- that is the player's explicit
// action and we leave it alone.
//
// Net result: hunter has bow + ammo -> full DPS; bow only ->
// shots still fire, no ammo DPS; no bow -> client engine's
// own ranged-weapon gate still blocks (Auto Shot timer
// simply never spins up without a ranged weapon equipped).
break;
};
case ITEM_SUBCLASS_WEAPON_WAND:
break;
default:
@@ -7852,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);
+9 -4
View File
@@ -1395,15 +1395,16 @@ bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod, Unit const* liste
{
case 53817: // Shaman: Maelstrom Weapon
{
// Allow any rank of Mage Fireball / Frostbolt / Arcane Blast to
// benefit from the cast-time + cost reduction spellmod.
// Allow any rank of Mage Fireball / Frostbolt to benefit from
// the cast-time + cost reduction spellmod. Arcane Blast was on
// the allowlist briefly but proved too potent stacked with its
// own self-buff -- removed.
if (SpellFamilyName == SPELLFAMILY_MAGE)
{
SpellInfo const* first = GetFirstRankSpell();
uint32 firstId = first ? first->Id : Id;
if (firstId == 133 /*Fireball*/
|| firstId == 116 /*Frostbolt*/
|| firstId == 30451 /*Arcane Blast*/)
|| firstId == 116 /*Frostbolt*/)
return true;
}
break;
@@ -2146,6 +2147,10 @@ SpellSpecificType SpellInfo::LoadSpellSpecific() const
{
case SPELLFAMILY_GENERIC:
{
// Fractured / Paragon: DK presence advancement clones (SpellFamilyName=0 in Spell.dbc for client UX).
if (Id == 951013 || Id == 951014 || Id == 951015)
return SPELL_SPECIFIC_PRESENCE;
// Food / Drinks (mostly)
if (AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED)
{
@@ -18,6 +18,7 @@
#include "DBCStores.h"
#include "DBCStructure.h"
#include "GameGraveyard.h"
#include "ItemTemplate.h"
#include "SpellInfo.h"
#include "SpellMgr.h"
@@ -5380,6 +5381,139 @@ void SpellMgr::LoadSpellInfoCorrections()
spellInfo->Attributes |= SPELL_ATTR0_PASSIVE;
});
// Fractured: move Death Knight Presences and Hunter Aspects out of
// SpellCategory 47 ("Combat States") so they cancel/toggle the same
// way Druid shapeshift forms do.
//
// Category 47 is the "stance bar" category. The 3.3.5a client UI
// explicitly disables right-click-cancel and `/cancelaura <name>` for
// any aura whose Spell.dbc Category column points at a SpellCategory
// entry that is "Combat States" (47). Druid forms (Bear Form, Cat
// Form, Travel Form, Moonkin, Tree of Life, etc.) sit in Category 0
// and are therefore freely cancellable -- right-click drops the form,
// /cancelaura drops it, recasting from the action bar drops it.
// Warrior stances, DK Presences and Hunter Aspects all live in
// Category 47, which is why none of them are cancellable in stock.
//
// For the cross-class stance / form / presence / aspect exclusivity
// rule (see IsFracturedExclusiveStanceSpell in Unit.cpp), a Paragon
// hybrid often wants to drop their active presence/aspect so they can
// apply a different stance/form *without* first switching to a
// different presence/aspect. Setting Category to 0 here mirrors what
// Druid forms already do, gives the cancel/toggle UX the user
// explicitly asked for, and -- importantly -- does NOT change the
// action bar (presences and aspects are not engine-shapeshifts, the
// bar swap behavior is owned by SPELL_AURA_MOD_SHAPESHIFT, not by
// SpellCategory). The matching client-side Spell.dbc edit ships in
// patch-enUS-4.MPQ via _patch_spell_dbc_presences_cancelable.py.
//
// Warrior stances are also included per design decision 2026-05-11
// ("you could make Warrior Stances toggleable as well, it should be
// okay"). The previously-shipped Stances=0 client patch already lets
// Paragon non-warriors cast every warrior ability without picking up
// a stance, so a stock warrior who right-clicks their stance just
// ends up at "no stance" -- which on this server still leaves all
// their warrior abilities available. Stock warriors who like the
// never-cancel UX can simply not right-click; nothing forces them.
//
// Tradeoff: stances / presences / aspects lose the 1s SpellCategory
// GCD that Category 47 enforces between same-category spells. This
// matches the Druid-form UX (Bear -> Cat -> Bear has no shared GCD),
// and the cross-class exclusivity rule in Aura::CanStackWith already
// prevents stacking, so the only thing actually possible at "0s GCD"
// is rapid-toggling the same stance on and off, which is harmless.
ApplySpellFix({
// Warrior Stances.
2457, // Battle Stance
71, // Defensive Stance
2458, // Berserker Stance
951010, 951011, 951012, // Paragon advancement warrior stance clones
// Death Knight Presences.
48266, // Blood Presence
48263, // Frost Presence
48265, // Unholy Presence
951013, 951014, 951015, // Paragon advancement DK presence clones (SpellFamily GENERIC in Spell.dbc)
// Hunter Aspects -- every rank, since AC stores the per-rank
// SpellInfo as separate objects and `Category` lives on each.
// Rank-1 ids are the same ones listed in
// IsFracturedExclusiveStanceSpell; trailing ids are higher ranks.
13165, 14318, 14319, 14320, 14321, 14322, 25296, 27044, // Aspect of the Hawk r1..r8
5118, // Aspect of the Cheetah
13159, // Aspect of the Pack (only one rank in WotLK; 27047 is "Growl", do NOT add)
20043, 20190, 27045, // Aspect of the Wild r1..r3
13161, // Aspect of the Beast
13163, // Aspect of the Monkey
34074, // Aspect of the Viper
61846, 61847, // Aspect of the Dragonhawk r1..r2
}, [](SpellInfo* spellInfo)
{
spellInfo->CategoryEntry = nullptr;
});
// Fractured: clear AttributesEx6 bit 0x1000 on Warrior Stances and DK
// Presences so the 3.3.5 client UI lets right-click and `/cancelaura`
// drop them, the same way Druid forms / Hunter Aspects already cancel.
//
// Empirical finding (see fractured-tooling/inspect_stance_attr6.py for
// the diff script): when only `SpellCategory` is cleared (the Combat-
// States gate at column 1), Hunter Aspects become cancellable but
// Warrior Stances and DK Presences still aren't. Diffing the Spell.dbc
// rows of working vs broken stance-bar buffs across patched-Aspects and
// unpatched-Stances/Presences identifies a SECOND gating column:
// `AttributesEx6` (col 10) bit `0x1000`. It is set on every Warrior
// Stance (Battle/Defensive/Berserker) and every DK Presence
// (Blood/Frost/Unholy) but NOT on any Hunter Aspect (and not on Druid
// forms / Ghost Wolf / Stealth / Shadowform). Clearing the bit removes
// the secondary client-UI gate without changing how the action bar /
// shapeshift system works (those are owned by SPELL_AURA_MOD_SHAPESHIFT,
// not by attribute bits).
//
// AC names this bit `SPELL_ATTR6_ALLOW_WHILE_RIDING_VEHICLE`. That name
// is from a different role of the same bit -- when set on a regular
// ability, AC's `Spell::CheckCast` vehicle-passenger gate uses it to
// grant "this spell is castable from a vehicle seat". Stripping it from
// Warrior Stances / DK Presences is harmless because those aren't cast
// from vehicle seats anyway (the player is `IsCharmed()` in a seat and
// the stance / presence wouldn't apply meaningfully). The matching
// client-side Spell.dbc edit ships in patch-enUS-4.MPQ via
// _patch_spell_dbc_presences_cancelable.py.
//
// Hunter Aspects intentionally NOT included -- their AttributesEx6 is
// already 0 (or 0x04000000 for Pack/Wild, which is a different bit
// unrelated to cancel gating), and listing them here would be a no-op.
ApplySpellFix({
2457, // Battle Stance
71, // Defensive Stance
2458, // Berserker Stance
951010, 951011, 951012, // Paragon advancement warrior stance clones
48266, // Blood Presence
48263, // Frost Presence
48265, // Unholy Presence
951013, 951014, 951015, // Paragon advancement DK presence clones (SpellFamily GENERIC in Spell.dbc)
}, [](SpellInfo* spellInfo)
{
spellInfo->AttributesEx6 &= ~SPELL_ATTR6_ALLOW_WHILE_RIDING_VEHICLE;
});
// Fractured / Paragon: advancement warrior stance clones — strip SPELL_ATTR1_NO_AURA_ICON
// (copied from stock 2457/71/2458). Stock Warrior stances intentionally hide from the default aura bar;
// these clones are meant to show a cancellable buff icon instead. Client Spell.dbc is patched in tandem via
// fractured-tooling/_patch_spell_dbc_paragon_stance_presence_clones.py.
ApplySpellFix({ 951010, 951011, 951012 }, [](SpellInfo* spellInfo)
{
spellInfo->AttributesEx &= ~SPELL_ATTR1_NO_AURA_ICON;
});
// Fractured / Paragon: advancement DK presence clones — strip SPELL_ATTR2_USE_SHAPESHIFT_BAR (0x10) copied
// from 48266/48263/48265. That client-only bit is what parks a spell on the secondary stance bar above the
// action bar; SkillLine / SpellFamily alone do not remove it. Spellbook tabs still come from SkillLines 770/771/772.
ApplySpellFix({ 951013, 951014, 951015 }, [](SpellInfo* spellInfo)
{
spellInfo->AttributesEx2 &= ~SPELL_ATTR2_USE_SHAPESHIFT_BAR;
});
// Fractured: strip reagent requirements from every player-class spell at
// load time. Filtered by SpellFamilyName != 0 so that profession spells
// (cooking, alchemy, enchanting, blacksmithing, jewelcrafting, leatherworking,
@@ -5418,6 +5552,76 @@ void SpellMgr::LoadSpellInfoCorrections()
LOG_INFO("server.loading", ">> Fractured: cleared reagents on {} class spells", fixedClassSpells);
}
// Fractured: drop EquippedItemClass on hunter shot abilities at load time
// so the server agrees with the matching client-side Spell.dbc patch
// (fractured-tooling/_patch_spell_dbc_hunter_ammo.py). Both surfaces have
// to agree -- if only the client patch shipped, the server's stock
// EquippedItemClass check would still reject mid-cast; if only the server
// mirror shipped, the 3.3.5a client preflight would still block the cast
// packet from leaving the box with "Ammo needs to be in the paper doll
// ammo slot before it can be fired." The Spell::CheckCast soft-fail
// (Spell.cpp 7741..) and the never-clear-PLAYER_AMMO_ID change there are
// still in place as defense in depth so a half-deployed client / server
// pair degrades to the soft-fail behavior rather than to hard rejects.
//
// Filter mirrors the Python patcher byte-for-byte:
// SpellFamilyName == SPELLFAMILY_HUNTER (9)
// AND EquippedItemClass == ITEM_CLASS_WEAPON (2)
// AND EquippedItemSubClassMask & ((1<<BOW)|(1<<GUN)|(1<<XBOW)) != 0
// with a small DENYLIST of item-equip-driven passive auras (Quiver /
// Ammo Pouch haste ranks, Legendary Bow Haste, Aynasha's Bow proc) whose
// entire purpose is "have a ranged weapon equipped" -- those keep their
// stock EquippedItemClass = 2.
//
// Effect: after this fix, hunter shots leave the client preflight without
// hitting the ammo-slot gate AND pass the server's EquippedItemClass
// check unconditionally. _ApplyAmmoBonuses still gates the arrow / bullet
// DPS bonus on actually having a stack in the quiver, so equipping ammo
// continues to give the DPS bump and an empty quiver no longer bricks
// abilities -- "you still get the DPS increase from arrows but aren't
// completely neutered if you run out", per the resident hunter expert.
{
constexpr uint32 RANGED_SUBCLASS_MASK =
(1u << ITEM_SUBCLASS_WEAPON_BOW)
| (1u << ITEM_SUBCLASS_WEAPON_GUN)
| (1u << ITEM_SUBCLASS_WEAPON_CROSSBOW);
// Keep in sync with DENYLIST in
// fractured-tooling/_patch_spell_dbc_hunter_ammo.py.
static const std::unordered_set<uint32> hunterAmmoDenylist = {
// Quiver / Ammo Pouch ranged-attack-speed haste passives (gun).
14824, 14825, 14826, 14827, 14828, 14829,
// Quiver passive haste (bow + crossbow).
29413, 29414, 29415, 29416, 29417, 29418,
// Late-rank quiver haste, gun-only.
44333,
// Legendary Bow Haste (item proc on a specific bow).
44972,
// Aynasha's Bow item proc.
19767,
};
uint32 fixedShots = 0;
for (uint32 spellId = 1; spellId < sSpellMgr->GetSpellInfoStoreSize(); ++spellId)
{
SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId);
if (!info || info->SpellFamilyName != SPELLFAMILY_HUNTER)
continue;
if (info->EquippedItemClass != ITEM_CLASS_WEAPON)
continue;
if (info->EquippedItemSubClassMask <= 0
|| (uint32(info->EquippedItemSubClassMask) & RANGED_SUBCLASS_MASK) == 0)
continue;
if (hunterAmmoDenylist.find(spellId) != hunterAmmoDenylist.end())
continue;
SpellInfo* mut = const_cast<SpellInfo*>(info);
mut->EquippedItemClass = -1;
++fixedShots;
}
LOG_INFO("server.loading", ">> Fractured: dropped EquippedItemClass on {} hunter shot abilities", fixedShots);
}
LOG_INFO("server.loading", ">> Loading spell dbc data corrections in {} ms", GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " ");
}
+97 -11
View File
@@ -66,6 +66,9 @@ enum DeathKnightSpells
SPELL_DK_ITEM_T8_MELEE_4P_BONUS = 64736,
SPELL_DK_MASTER_OF_GHOULS = 52143,
SPELL_DK_BLOOD_PLAGUE = 55078,
// Fractured / Paragon: stock Priest Devouring Plague vs Character Advancement multidot clone
SPELL_PRIEST_DEVOURING_PLAGUE_R1 = 2944,
SPELL_PARAGON_MULTIDOT_DEVOURING_PLAGUE_R1 = 951000,
SPELL_DK_RAISE_DEAD_USE_REAGENT = 48289,
SPELL_DK_RUNIC_POWER_ENERGIZE = 49088,
SPELL_DK_SCENT_OF_BLOOD = 50422,
@@ -107,6 +110,10 @@ enum DeathKnightSpells
SPELL_DK_RUNE_STRIKE_OFF_HAND_R1 = 66217,
SPELL_DK_BLOOD_STRIKE_OFF_HAND_R1 = 66215,
SPELL_DK_KILLING_MACHINE = 51124,
// Fractured / Paragon: Character Advancement DK presence clones (SpellFamily GENERIC in Spell.dbc).
SPELL_PARAGON_ADV_BLOOD_PRESENCE = 951013,
SPELL_PARAGON_ADV_FROST_PRESENCE = 951014,
SPELL_PARAGON_ADV_UNHOLY_PRESENCE = 951015,
};
enum DeathKnightSpellIcons
@@ -126,6 +133,21 @@ enum Misc
NPC_RISEN_ALLY = 30230
};
inline bool Fractured_UnitHasBloodPresenceAura(Unit const* unit)
{
return unit->HasAura(SPELL_DK_BLOOD_PRESENCE) || unit->HasAura(SPELL_PARAGON_ADV_BLOOD_PRESENCE);
}
inline bool Fractured_UnitHasFrostPresenceAura(Unit const* unit)
{
return unit->HasAura(SPELL_DK_FROST_PRESENCE) || unit->HasAura(SPELL_PARAGON_ADV_FROST_PRESENCE);
}
inline bool Fractured_UnitHasUnholyPresenceAura(Unit const* unit)
{
return unit->HasAura(SPELL_DK_UNHOLY_PRESENCE) || unit->HasAura(SPELL_PARAGON_ADV_UNHOLY_PRESENCE);
}
// 50526 - Wandering Plague
class spell_dk_wandering_plague : public SpellScript
{
@@ -664,6 +686,23 @@ class spell_dk_dancing_rune_weapon : public AuraScript
return false;
SpellInfo const* spellInfo = eventInfo.GetSpellInfo();
// Fractured / Paragon: Paragon owners get a "ghostly weapon copies
// your swings" identity instead of the stock magical-doppelganger
// (which also re-cast Death Coil / Icy Touch / Howling Blast /
// etc.). For Paragon callers only, accept auto-attacks and
// melee-class abilities (Hamstring, Sinister Strike, Heart Strike,
// Frost Strike, Mortal Strike, ...) and reject magic / ranged
// spells. Stock DK gating below is left untouched.
if (IsParagonWildcardCaller(eventInfo.GetActor()))
{
if (!eventInfo.GetDamageInfo())
return false;
if (spellInfo && spellInfo->DmgClass != SPELL_DAMAGE_CLASS_MELEE)
return false;
return true;
}
if (!spellInfo)
return true;
@@ -1780,8 +1819,11 @@ class spell_dk_improved_blood_presence : public AuraScript
return ValidateSpellInfo(
{
SPELL_DK_BLOOD_PRESENCE,
SPELL_PARAGON_ADV_BLOOD_PRESENCE,
SPELL_DK_FROST_PRESENCE,
SPELL_PARAGON_ADV_FROST_PRESENCE,
SPELL_DK_UNHOLY_PRESENCE,
SPELL_PARAGON_ADV_UNHOLY_PRESENCE,
SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED
});
}
@@ -1789,14 +1831,14 @@ class spell_dk_improved_blood_presence : public AuraScript
void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/)
{
Unit* target = GetTarget();
if (target->HasAnyAuras(SPELL_DK_FROST_PRESENCE, SPELL_DK_UNHOLY_PRESENCE) && !target->HasAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED))
if (target->HasAnyAuras(SPELL_DK_FROST_PRESENCE, SPELL_DK_UNHOLY_PRESENCE, SPELL_PARAGON_ADV_FROST_PRESENCE, SPELL_PARAGON_ADV_UNHOLY_PRESENCE) && !target->HasAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED))
target->CastCustomSpell(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED, SPELLVALUE_BASE_POINT1, aurEff->GetAmount(), target, true, nullptr, aurEff);
}
void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)
{
Unit* target = GetTarget();
if (!target->HasAura(SPELL_DK_BLOOD_PRESENCE))
if (!Fractured_UnitHasBloodPresenceAura(target))
target->RemoveAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED);
}
@@ -1817,8 +1859,11 @@ class spell_dk_improved_frost_presence : public AuraScript
return ValidateSpellInfo(
{
SPELL_DK_BLOOD_PRESENCE,
SPELL_PARAGON_ADV_BLOOD_PRESENCE,
SPELL_DK_FROST_PRESENCE,
SPELL_PARAGON_ADV_FROST_PRESENCE,
SPELL_DK_UNHOLY_PRESENCE,
SPELL_PARAGON_ADV_UNHOLY_PRESENCE,
SPELL_DK_FROST_PRESENCE_TRIGGERED
});
}
@@ -1826,14 +1871,14 @@ class spell_dk_improved_frost_presence : public AuraScript
void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/)
{
Unit* target = GetTarget();
if (target->HasAnyAuras(SPELL_DK_BLOOD_PRESENCE, SPELL_DK_UNHOLY_PRESENCE) && !target->HasAura(SPELL_DK_FROST_PRESENCE_TRIGGERED))
if (target->HasAnyAuras(SPELL_DK_BLOOD_PRESENCE, SPELL_DK_UNHOLY_PRESENCE, SPELL_PARAGON_ADV_BLOOD_PRESENCE, SPELL_PARAGON_ADV_UNHOLY_PRESENCE) && !target->HasAura(SPELL_DK_FROST_PRESENCE_TRIGGERED))
target->CastCustomSpell(SPELL_DK_FROST_PRESENCE_TRIGGERED, SPELLVALUE_BASE_POINT0, aurEff->GetAmount(), target, true, nullptr, aurEff);
}
void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)
{
Unit* target = GetTarget();
if (!target->HasAura(SPELL_DK_FROST_PRESENCE))
if (!Fractured_UnitHasFrostPresenceAura(target))
target->RemoveAura(SPELL_DK_FROST_PRESENCE_TRIGGERED);
}
@@ -1854,8 +1899,11 @@ class spell_dk_improved_unholy_presence : public AuraScript
return ValidateSpellInfo(
{
SPELL_DK_BLOOD_PRESENCE,
SPELL_PARAGON_ADV_BLOOD_PRESENCE,
SPELL_DK_FROST_PRESENCE,
SPELL_PARAGON_ADV_FROST_PRESENCE,
SPELL_DK_UNHOLY_PRESENCE,
SPELL_PARAGON_ADV_UNHOLY_PRESENCE,
SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED,
SPELL_DK_UNHOLY_PRESENCE_TRIGGERED
});
@@ -1864,14 +1912,14 @@ class spell_dk_improved_unholy_presence : public AuraScript
void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/)
{
Unit* target = GetTarget();
if (target->HasAura(SPELL_DK_UNHOLY_PRESENCE) && !target->HasAura(SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED))
if (Fractured_UnitHasUnholyPresenceAura(target) && !target->HasAura(SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED))
{
// Not listed as any effect, only base points set in dbc
int32 basePoints = GetSpellInfo()->Effects[EFFECT_1].CalcValue();
target->CastCustomSpell(target, SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED, &basePoints, &basePoints, &basePoints, true, nullptr, aurEff);
}
if (target->HasAnyAuras(SPELL_DK_BLOOD_PRESENCE, SPELL_DK_FROST_PRESENCE) && !target->HasAura(SPELL_DK_UNHOLY_PRESENCE_TRIGGERED))
if (target->HasAnyAuras(SPELL_DK_BLOOD_PRESENCE, SPELL_DK_FROST_PRESENCE, SPELL_PARAGON_ADV_BLOOD_PRESENCE, SPELL_PARAGON_ADV_FROST_PRESENCE) && !target->HasAura(SPELL_DK_UNHOLY_PRESENCE_TRIGGERED))
target->CastCustomSpell(SPELL_DK_UNHOLY_PRESENCE_TRIGGERED, SPELLVALUE_BASE_POINT0, aurEff->GetAmount(), target, true, nullptr, aurEff);
}
@@ -1881,7 +1929,7 @@ class spell_dk_improved_unholy_presence : public AuraScript
target->RemoveAura(SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED);
if (!target->HasAura(SPELL_DK_UNHOLY_PRESENCE))
if (!Fractured_UnitHasUnholyPresenceAura(target))
target->RemoveAura(SPELL_DK_UNHOLY_PRESENCE_TRIGGERED);
}
@@ -1916,6 +1964,20 @@ class spell_dk_pestilence : public SpellScript
if (!target)
return;
// Fractured / Paragon: when the Pestilence caster is a Paragon and
// wildcard family matching is on, also spread (or refresh) Priest
// Devouring Plague. Devouring Plague's Dispel field is DISPEL_DISEASE
// and Unit::GetDiseasesByCaster already counts it for Paragon callers
// (see Unit.cpp), so it is conceptually a disease; stock Pestilence
// just hard-codes Blood Plague + Frost Fever and so silently drops it.
// GetAuraOfRankedSpell with the rank-1 id (2944 / 951000) covers every rank of
// Devouring Plague the player has on the target -- we re-cast that
// exact same rank so the spread copy carries the caster's actual
// damage tier rather than always rank 1. Stock DKs cannot cast
// Devouring Plague at all, so both lookups return null for them and
// this branch is a no-op there.
bool const paragonSpread = IsParagonWildcardCaller(caster);
// Spread on others
if (target != hitUnit)
{
@@ -1926,6 +1988,17 @@ class spell_dk_pestilence : public SpellScript
// Frost Fever
if (target->GetAura(SPELL_DK_FROST_FEVER, caster->GetGUID()))
caster->CastSpell(hitUnit, SPELL_DK_FROST_FEVER, true);
// Fractured / Paragon: Devouring Plague spread (stock 2944 chain or
// Character Advancement multidot clone 951000 chain).
if (paragonSpread)
{
Aura const* dp = target->GetAuraOfRankedSpell(SPELL_PRIEST_DEVOURING_PLAGUE_R1, caster->GetGUID());
if (!dp)
dp = target->GetAuraOfRankedSpell(SPELL_PARAGON_MULTIDOT_DEVOURING_PLAGUE_R1, caster->GetGUID());
if (dp)
caster->CastSpell(hitUnit, dp->GetId(), true);
}
}
// Refresh on target
else if (caster->GetAura(SPELL_DK_GLYPH_OF_DISEASE))
@@ -1946,6 +2019,16 @@ class spell_dk_pestilence : public SpellScript
disease->RefreshDuration();
else if (Aura* disease = target->GetAuraOfRankedSpell(SPELL_DK_CRYPT_FEVER_R1, caster->GetGUID()))
disease->RefreshDuration();
// Fractured / Paragon: Devouring Plague Glyph-of-Disease refresh.
if (paragonSpread)
{
Aura* dp = target->GetAuraOfRankedSpell(SPELL_PRIEST_DEVOURING_PLAGUE_R1, caster->GetGUID());
if (!dp)
dp = target->GetAuraOfRankedSpell(SPELL_PARAGON_MULTIDOT_DEVOURING_PLAGUE_R1, caster->GetGUID());
if (dp)
dp->RefreshDuration();
}
}
}
@@ -1969,6 +2052,9 @@ class spell_dk_presence : public AuraScript
SPELL_DK_BLOOD_PRESENCE,
SPELL_DK_FROST_PRESENCE,
SPELL_DK_UNHOLY_PRESENCE,
SPELL_PARAGON_ADV_BLOOD_PRESENCE,
SPELL_PARAGON_ADV_FROST_PRESENCE,
SPELL_PARAGON_ADV_UNHOLY_PRESENCE,
SPELL_DK_IMPROVED_BLOOD_PRESENCE_R1,
SPELL_DK_IMPROVED_FROST_PRESENCE_R1,
SPELL_DK_IMPROVED_UNHOLY_PRESENCE_R1,
@@ -1983,7 +2069,7 @@ class spell_dk_presence : public AuraScript
{
Unit* target = GetTarget();
if (GetId() == SPELL_DK_BLOOD_PRESENCE)
if (GetId() == SPELL_DK_BLOOD_PRESENCE || GetId() == SPELL_PARAGON_ADV_BLOOD_PRESENCE)
target->CastSpell(target, SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED, true);
else if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_BLOOD_PRESENCE_R1, EFFECT_0))
if (!target->HasAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED))
@@ -1994,7 +2080,7 @@ class spell_dk_presence : public AuraScript
{
Unit* target = GetTarget();
if (GetId() == SPELL_DK_FROST_PRESENCE)
if (GetId() == SPELL_DK_FROST_PRESENCE || GetId() == SPELL_PARAGON_ADV_FROST_PRESENCE)
target->CastSpell(target, SPELL_DK_FROST_PRESENCE_TRIGGERED, true);
else if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_FROST_PRESENCE_R1, EFFECT_0))
if (!target->HasAura(SPELL_DK_FROST_PRESENCE_TRIGGERED))
@@ -2005,12 +2091,12 @@ class spell_dk_presence : public AuraScript
{
Unit* target = GetTarget();
if (GetId() == SPELL_DK_UNHOLY_PRESENCE)
if (GetId() == SPELL_DK_UNHOLY_PRESENCE || GetId() == SPELL_PARAGON_ADV_UNHOLY_PRESENCE)
target->CastSpell(target, SPELL_DK_UNHOLY_PRESENCE_TRIGGERED, true);
if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_UNHOLY_PRESENCE_R1, EFFECT_0))
{
if (GetId() == SPELL_DK_UNHOLY_PRESENCE)
if (GetId() == SPELL_DK_UNHOLY_PRESENCE || GetId() == SPELL_PARAGON_ADV_UNHOLY_PRESENCE)
{
// Not listed as any effect, only base points set
int32 bp = impAurEff->GetSpellInfo()->Effects[EFFECT_1].CalcValue();
+5 -4
View File
@@ -1793,8 +1793,10 @@ class spell_sha_maelstrom_weapon : public AuraScript
// Fractured / Paragon: spell_proc row 53817 is data-relaxed (wildcard
// family/mask) so cross-class spells can reach this CheckProc. We
// restore the original Shaman gating here for stock callers and add
// the Paragon-only Mage Fireball / Frostbolt / Arcane Blast allowlist
// mirroring the IsAffectedBySpellMod hook in SpellInfo.cpp.
// the Paragon-only Mage Fireball / Frostbolt allowlist mirroring the
// IsAffectedBySpellMod hook in SpellInfo.cpp. Arcane Blast was on the
// allowlist briefly but proved too potent stacked with its own
// self-buff -- removed.
bool CheckProc(ProcEventInfo& eventInfo)
{
SpellInfo const* procSpell = eventInfo.GetSpellInfo();
@@ -1820,8 +1822,7 @@ class spell_sha_maelstrom_weapon : public AuraScript
SpellInfo const* first = procSpell->GetFirstRankSpell();
uint32 firstId = first ? first->Id : procSpell->Id;
if (firstId == 133 /*Fireball*/
|| firstId == 116 /*Frostbolt*/
|| firstId == 30451 /*Arcane Blast*/)
|| firstId == 116 /*Frostbolt*/)
return true;
}