Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a1f8eec89 | |||
| 0bb6b0ef84 | |||
| 295cb6df52 | |||
| fbd6ea47f2 | |||
| a64279ed7e | |||
| 87219cb4eb | |||
| da17074a63 | |||
| b8826370c6 | |||
| d1d68cb44a | |||
| 999f7e94bd | |||
| 7c57abd69f | |||
| e649402163 |
@@ -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).
|
||||||
@@ -21,7 +21,7 @@ when a release is published here (workflow **Sync release to Fractured-Distro**)
|
|||||||
| Artifact | Size | Purpose |
|
| Artifact | Size | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `patch-enUS-4.MPQ` | ~5 MB | DBC + GlueXML bake. Adds `CLASS_PARAGON` (id 12), the character-create slot, glue strings, game-table DBCs, and a patched `Spell.dbc`: **(1)** `RuneCostID` zeroed on every rune-cost spell so non–Death Knight clients still send DK casts (rune costs are shown via `RuneFrame.lua`); **(2)** `Reagent[]` / `ReagentCount[]` zeroed on every spell whose `SpellFamilyName` is non-zero (all class abilities), while profession crafts (`SpellFamilyName == 0`) keep their materials. Both edits mirror server load-time corrections so client preflight and server validation stay aligned. Required for character creation as Paragon to even show up. |
|
| `patch-enUS-4.MPQ` | ~5 MB | DBC + GlueXML bake. Adds `CLASS_PARAGON` (id 12), the character-create slot, glue strings, game-table DBCs, and a patched `Spell.dbc`: **(1)** `RuneCostID` zeroed on every rune-cost spell so non–Death Knight clients still send DK casts (rune costs are shown via `RuneFrame.lua`); **(2)** `Reagent[]` / `ReagentCount[]` zeroed on every spell whose `SpellFamilyName` is non-zero (all class abilities), while profession crafts (`SpellFamilyName == 0`) keep their materials. Both edits mirror server load-time corrections so client preflight and server validation stay aligned. Required for character creation as Paragon to even show up. |
|
||||||
| `patch-enUS-5.MPQ` | ~57 KB | FrameXML overrides. Replaces stock `PlayerFrame.lua` / `RuneFrame.lua` / `ComboFrame.lua` / `UnitFrame.lua` / `SpellBookFrame.lua` + `SpellBookFrame.xml` with Paragon-aware versions: rune simulator, combo-point simulator, server-authoritative resource sync over the `PARAA` addon channel, action-button usability + click guards, an expanded spellbook (higher `MAX_SPELLS`, 24 skill-line tabs instead of stock 8) so all-class spells render, Paragon stat tooltips on the character sheet (including filtering duplicate “attack power from strength” lines so the paper doll matches server AP), a tooltip post-processor that appends ", Paragon" to the "Classes:" line on class-restricted gear / glyphs (the server bypasses `AllowableClass` for class 12, but the engine paints the line red and omits Paragon — the Lua hook recolors it green and adds the name so the player can tell it's wearable), and **PetFrame** re-anchored so the **pet unit frame sits below the rune row** for Paragon (stock layout had runes overlapping the pet portrait). The paper-doll **ammo slot** follows stock visibility rules (shown for hunters / ranged weapons; hidden when `UnitHasRelicSlot` applies). |
|
| `patch-enUS-5.MPQ` | ~64 KB | FrameXML overrides. Replaces stock `PlayerFrame.lua` / `RuneFrame.lua` / `ComboFrame.lua` / `UnitFrame.lua` / `SpellBookFrame.lua` + `SpellBookFrame.xml` with Paragon-aware versions: rune simulator, combo-point simulator, server-authoritative resource sync over the `PARAA` addon channel, action-button usability + click guards, an expanded spellbook (higher `MAX_SPELLS`, 24 skill-line tabs instead of stock 8) so all-class spells render, Paragon stat tooltips on the character sheet (including filtering duplicate “attack power from strength” lines so the paper doll matches server AP), a tooltip post-processor that appends ", Paragon" to the "Classes:" line on class-restricted gear / glyphs (the server bypasses `AllowableClass` for class 12, but the engine paints the line red and omits Paragon — the Lua hook recolors it green and adds the name so the player can tell it's wearable), a **spell-tooltip post-processor** that (1) recolors and appends "(Paragon: bypassed)" to "Requires *Stance*" lines on Warrior abilities (server-side `SpellInfo::CheckShapeshift` skips stance enforcement for Paragons on `SPELLFAMILY_WARRIOR` spells, but the client still renders the requirement from the stock `Stances` DBC field which we deliberately leave unzeroed so stock Warriors keep enforcement), (2) appends a Paragon line to **Maelstrom Weapon** (53817) noting that Fireball / Frostbolt / Arcane Blast also benefit, and (3) appends a Paragon line to **Mirror Image** (55342) noting that the images cast random damage spells with a cast time from the caster's spellbook instead of Frostbolt — all three patches gate on `UnitClass("player") == "PARAGON"` so stock-class tooltips are byte-identical to vanilla, and **PetFrame** re-anchored so the **pet unit frame sits below the rune row** for Paragon (stock layout had runes overlapping the pet portrait). A **Warrior stance click bypass** wraps `UseAction` so that for Paragon characters, clicking an action slot bound to a stance-gated Warrior ability (Whirlwind, Charge, Pummel, Shield Slam, Hamstring, Overpower, Shield Bash, Shield Block, Disarm, Revenge, Spell Reflection, Recklessness, Bladestorm, Shockwave, Concussion Blow, Last Stand, Sweeping Strikes, Mocking Blow, Heroic Fury, Slam, Devastate, Intercept) routes through `CastSpellByName(name)` instead of the engine's stance-gated `UseAction` path; the engine's stance pre-check inside `UseAction` would otherwise drop the cast packet client-side and our server-side `CheckShapeshift` bypass would never get to run. Stock classes never enter the bypass branch. The paper-doll **ammo slot** follows stock visibility rules (shown for hunters / ranged weapons; hidden when `UnitHasRelicSlot` applies). |
|
||||||
| `patch-enUS-6.MPQ` | ~134 KB | The `ParagonAdvancement` addon. Replaces the talent pane (`N` key) for Paragon characters with the Character Advancement panel: per-class spell tabs, talent grid, Overview/Search tabs, AE/TE currency, commit / reset / preview, login-time toast suppression, a **PETS** tab with live hunter pet talent trees (preview learn, no TE/AE cost), a dedicated **Reset Pet Talents** control (server `PARAA` `C RESET PET TALENTS` — instant, no gold, no confirmation; requires matching worldserver), bottom-row **Reset all Abilities / Reset Build / Reset all Talents** disabled while on the PETS tab so those paths cannot dismiss the pet or unlearn Tame Beast, and a **Builds** page (full-pane overlay opened from the bottom-row Builds button) for saving named, icon-tagged loadouts: New Build (+) icon picker reuses `MACRO_ICON_FILENAMES`, right-click for edit/delete, shift-left-click to favorite (favorites bubble to the top), left-click pops a Load Build confirm. Build swaps reset + refund AE/TE, re-spend on the saved recipe, and **park hunter pets** to `PET_SAVE_NOT_IN_SLOT` so their name/talents/exp are preserved across swaps. |
|
| `patch-enUS-6.MPQ` | ~134 KB | The `ParagonAdvancement` addon. Replaces the talent pane (`N` key) for Paragon characters with the Character Advancement panel: per-class spell tabs, talent grid, Overview/Search tabs, AE/TE currency, commit / reset / preview, login-time toast suppression, a **PETS** tab with live hunter pet talent trees (preview learn, no TE/AE cost), a dedicated **Reset Pet Talents** control (server `PARAA` `C RESET PET TALENTS` — instant, no gold, no confirmation; requires matching worldserver), bottom-row **Reset all Abilities / Reset Build / Reset all Talents** disabled while on the PETS tab so those paths cannot dismiss the pet or unlearn Tame Beast, and a **Builds** page (full-pane overlay opened from the bottom-row Builds button) for saving named, icon-tagged loadouts: New Build (+) icon picker reuses `MACRO_ICON_FILENAMES`, right-click for edit/delete, shift-left-click to favorite (favorites bubble to the top), left-click pops a Load Build confirm. Build swaps reset + refund AE/TE, re-spend on the saved recipe, and **park hunter pets** to `PET_SAVE_NOT_IN_SLOT` so their name/talents/exp are preserved across swaps. |
|
||||||
| `Wow.exe` | ~7.5 MB | 3.3.5a (build 12340) client byte-patched to skip the MPQ signature check so custom `patch-enUS-N.MPQ` files load. Diff against stock is a few bytes; everything else is unchanged. |
|
| `Wow.exe` | ~7.5 MB | 3.3.5a (build 12340) client byte-patched to skip the MPQ signature check so custom `patch-enUS-N.MPQ` files load. Diff against stock is a few bytes; everything else is unchanged. |
|
||||||
|
|
||||||
@@ -251,7 +251,8 @@ order on a maintainer machine:
|
|||||||
|
|
||||||
1. `fractured-tooling/from-workspace-root/_patch_spell_dbc_runes.py` — stage `Spell.dbc` with `RuneCostID` cleared.
|
1. `fractured-tooling/from-workspace-root/_patch_spell_dbc_runes.py` — stage `Spell.dbc` with `RuneCostID` cleared.
|
||||||
2. `fractured-tooling/from-workspace-root/_patch_spell_dbc_reagents.py` — same staged `Spell.dbc`, clear class-spell reagents for client preflight.
|
2. `fractured-tooling/from-workspace-root/_patch_spell_dbc_reagents.py` — same staged `Spell.dbc`, clear class-spell reagents for client preflight.
|
||||||
3. `fractured-tooling/from-workspace-root/_make_paragon_dbc_patch.py` — rebuild `ChrClasses` / `CharBaseInfo` / game tables, then pack `patch-enUS-4.MPQ`.
|
3. `fractured-tooling/from-workspace-root/_patch_spell_dbc_stances.py` — same staged `Spell.dbc`, zero the `Stances` field on every `SpellFamilyName == 4` (Warrior) row so the client engine's "Must be in Battle/Defensive/Berserker Stance" pre-cast check stops eating `CMSG_CAST_SPELL` packets for Paragon casters who never picked the stance form. The server's `SpellInfo::CheckShapeshift` Paragon bypass takes over from there. Stock Warriors still see the server-side stance error mid-cast if they actually click while out of stance.
|
||||||
|
4. `fractured-tooling/from-workspace-root/_make_paragon_dbc_patch.py` — rebuild `ChrClasses` / `CharBaseInfo` / game tables, then pack `patch-enUS-4.MPQ`.
|
||||||
|
|
||||||
The patched `Wow.exe` is a one-time hex-edit of the stock 3.3.5a
|
The patched `Wow.exe` is a one-time hex-edit of the stock 3.3.5a
|
||||||
client. The diff is publicly documented in the WoW emulation community
|
client. The diff is publicly documented in the WoW emulation community
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ AzerothCore. Upstream AzerothCore does not ship these paths.
|
|||||||
|
|
||||||
Contents:
|
Contents:
|
||||||
- BUILD-NATIVE.md — fork-specific native build notes (moved from repo root).
|
- 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).
|
- CLAUDE.md — optional AI assistant context (moved from repo root).
|
||||||
- CLIENT-PATCHES.md — what ships in a Fractured client release (MPQs +
|
- CLIENT-PATCHES.md — what ships in a Fractured client release (MPQs +
|
||||||
patched Wow.exe), where to download them (Releases page), and how
|
patched Wow.exe), where to download them (Releases page), and how
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- DB update 2026_05_03_00 -> 2026_05_12_00
|
||||||
|
-- RBAC permission for .learn all mounts (Admin 196, Gamemaster 197).
|
||||||
|
DELETE FROM `rbac_permissions` WHERE `id` = 916;
|
||||||
|
INSERT INTO `rbac_permissions` (`id`, `name`) VALUES
|
||||||
|
(916, 'Command: learn all mounts');
|
||||||
|
|
||||||
|
DELETE FROM `rbac_linked_permissions` WHERE `linkedId` = 916;
|
||||||
|
INSERT INTO `rbac_linked_permissions` (`id`, `linkedId`) VALUES
|
||||||
|
(196, 916),
|
||||||
|
(197, 916);
|
||||||
@@ -472,6 +472,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
|||||||
(61999, 1),
|
(61999, 1),
|
||||||
(62078, 1),
|
(62078, 1),
|
||||||
(62124, 1),
|
(62124, 1),
|
||||||
|
(62600, 1),
|
||||||
(62757, 1),
|
(62757, 1),
|
||||||
(64382, 1),
|
(64382, 1),
|
||||||
(64843, 1),
|
(64843, 1),
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
-- mod-paragon: Vampiric Embrace (15286) cross-family wildcard.
|
||||||
|
--
|
||||||
|
-- Stock 3.3.5 spell_proc row for Vampiric Embrace gates by SpellFamilyName=6
|
||||||
|
-- (PRIEST) plus a Priest-Shadow-damage SpellFamilyMask. That blocks the proc
|
||||||
|
-- engine from ever calling the AuraScript's CheckProc when a Paragon casts a
|
||||||
|
-- non-Priest Shadow-school spell (e.g. Warlock Shadow Bolt, Death Knight
|
||||||
|
-- Death Coil, etc.), because IsAffected's familyFlags gate fails before
|
||||||
|
-- CheckProc runs even though the Phase A wildcard already loosens the
|
||||||
|
-- familyName equality test (see SpellInfo::IsAffected, listenerOwner overload).
|
||||||
|
--
|
||||||
|
-- We relax this row so:
|
||||||
|
-- * SchoolMask=32 (SHADOW) kept -- proc-engine still gates by school
|
||||||
|
-- * SpellTypeMask=1 (DAMAGE) kept -- only damage events trigger CheckProc
|
||||||
|
-- * SpellPhaseMask=2 (HIT) kept -- post-hit phase
|
||||||
|
-- * AttributesMask=2 (TRIGGERED) kept -- triggered-spell payloads still proc
|
||||||
|
-- * SpellFamilyName=0 wildcard -- IsAffected short-circuits to true
|
||||||
|
-- * SpellFamilyMask{0,1,2}=0 wildcard -- no flag-bit gating at this layer
|
||||||
|
--
|
||||||
|
-- Real filtering moves into spell_pri_vampiric_embrace::CheckProc, which
|
||||||
|
-- branches on IsParagonWildcardCaller(GetTarget()):
|
||||||
|
--
|
||||||
|
-- * For Paragon owners with `Paragon.WildcardFamilyMatching = 1`, accept any
|
||||||
|
-- single-target Shadow-school spell (Mind Sear / AoE shadow spells like
|
||||||
|
-- Seed of Corruption / Hellfire are filtered there via IsAffectingArea
|
||||||
|
-- and the existing Mind Sear bit-mask).
|
||||||
|
--
|
||||||
|
-- * For stock Priest owners (and for the non-wildcard runtime path), the
|
||||||
|
-- CheckProc re-enforces the EXACT original gate -- SpellFamilyName=6 plus
|
||||||
|
-- the original 0x0280A010 / 0x00002402 / 0x00000008 SpellFamilyMask bits
|
||||||
|
-- -- so behavior is byte-identical to before this change for any caster
|
||||||
|
-- that is not a Paragon.
|
||||||
|
--
|
||||||
|
-- Net effect: Paragon characters with VE learned now leech-heal off any
|
||||||
|
-- single-target Shadow spell they cast (Death Coil, Shadow Bolt, Searing
|
||||||
|
-- Pain, Drain Soul, etc.); stock Shadow Priests are unchanged.
|
||||||
|
|
||||||
|
DELETE FROM `spell_proc` WHERE `SpellId` = 15286;
|
||||||
|
|
||||||
|
INSERT INTO `spell_proc`
|
||||||
|
(`SpellId`, `SchoolMask`, `SpellFamilyName`,
|
||||||
|
`SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`,
|
||||||
|
`ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`,
|
||||||
|
`AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`,
|
||||||
|
`Chance`, `Cooldown`, `Charges`)
|
||||||
|
VALUES
|
||||||
|
(15286, 32, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 0, 0, 0);
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
-- mod-paragon: Maelstrom Weapon (53817) cross-family wildcard.
|
||||||
|
--
|
||||||
|
-- Stock 3.3.5 spell_proc row for Maelstrom Weapon gates by SpellFamilyName=11
|
||||||
|
-- (SHAMAN) plus a Shaman SpellFamilyMask covering Lightning Bolt, Chain
|
||||||
|
-- Lightning, Lesser Healing Wave, Healing Wave and Hex (Mask0=451,
|
||||||
|
-- Mask1=32768). The proc engine therefore never delivers an event to the
|
||||||
|
-- AuraScript when a Paragon casts a non-Shaman cast-time spell, even if the
|
||||||
|
-- IsAffected wildcard relaxes SpellFamilyName equality (the SpellFamilyMask
|
||||||
|
-- AND-with-target-FamilyFlags check still fails because Mage / Warlock /
|
||||||
|
-- Druid spell-class bits do not overlap with Shaman bits).
|
||||||
|
--
|
||||||
|
-- We relax this row so:
|
||||||
|
-- * SchoolMask=0 wildcard -- proc engine no longer gates by school
|
||||||
|
-- * SpellTypeMask=1 (DAMAGE) kept -- only damage spells trigger CheckProc
|
||||||
|
-- * SpellPhaseMask=8 (FINISH) kept -- post-cast phase, on cast finish
|
||||||
|
-- * SpellFamilyName=0 wildcard -- IsAffected short-circuits to true
|
||||||
|
-- * SpellFamilyMask{0,1,2}=0 wildcard -- no flag-bit gating at this layer
|
||||||
|
--
|
||||||
|
-- Real filtering moves into spell_sha_maelstrom_weapon::CheckProc:
|
||||||
|
--
|
||||||
|
-- * For stock Shaman owners (and for the non-wildcard runtime path), the
|
||||||
|
-- CheckProc re-enforces the EXACT original gate -- SpellFamilyName=11
|
||||||
|
-- plus the original Mask0=451 / Mask1=32768 bits -- so behavior is
|
||||||
|
-- byte-identical to before this change for any caster that is not a
|
||||||
|
-- Paragon.
|
||||||
|
--
|
||||||
|
-- * For Paragon owners with `Paragon.WildcardFamilyMatching = 1`, the
|
||||||
|
-- stock allowlist still passes, AND we additionally accept the curated
|
||||||
|
-- Mage cast-time nukes Fireball / Frostbolt / Arcane Blast (any rank,
|
||||||
|
-- matched via GetFirstRankSpell).
|
||||||
|
--
|
||||||
|
-- The matching IsAffectedBySpellMod hook in SpellInfo.cpp ensures the cast
|
||||||
|
-- time + power cost spellmods on aura 53817 also bridge across families for
|
||||||
|
-- the same Mage spell allowlist, so Paragons get the full Maelstrom Weapon
|
||||||
|
-- experience (instant cast at 5 stacks + reduced mana cost) on Fireball,
|
||||||
|
-- Frostbolt and Arcane Blast.
|
||||||
|
--
|
||||||
|
-- Net effect: Paragon characters with Maelstrom Weapon learned now spend
|
||||||
|
-- stacks on Mage cast-time nukes in addition to the stock Shaman list;
|
||||||
|
-- stock Enhancement Shamans are unchanged.
|
||||||
|
|
||||||
|
DELETE FROM `spell_proc` WHERE `SpellId` = 53817;
|
||||||
|
|
||||||
|
INSERT INTO `spell_proc`
|
||||||
|
(`SpellId`, `SchoolMask`, `SpellFamilyName`,
|
||||||
|
`SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`,
|
||||||
|
`ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`,
|
||||||
|
`AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`,
|
||||||
|
`Chance`, `Cooldown`, `Charges`)
|
||||||
|
VALUES
|
||||||
|
(53817, 0, 0, 0, 0, 0, 0, 1, 8, 0, 0, 0, 0, 0, 0, 0);
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
-- mod-paragon: Frostbite + Fingers of Frost cross-family wildcard.
|
||||||
|
--
|
||||||
|
-- Both talents are normally gated by SpellFamilyName=3 (MAGE) plus a Mage
|
||||||
|
-- SpellFamilyMask covering specific Mage Frost spells (Frostbolt / Frost Nova
|
||||||
|
-- / Cone of Cold / Blizzard / Frostfire Bolt / Deep Freeze for FoF; Frost
|
||||||
|
-- slow-applying spells for Frostbite). That blocks the proc engine from
|
||||||
|
-- delivering an event to the AuraScript when a Paragon casts a non-Mage
|
||||||
|
-- Frost-school chill effect (DK Howling Blast / Icy Touch / Chains of Ice,
|
||||||
|
-- Hunter Frost Trap, Shaman Frost Shock, etc.), because IsAffected's
|
||||||
|
-- familyFlags AND-with-target-FamilyFlags check fails before CheckProc runs
|
||||||
|
-- even after the Paragon family-name wildcard.
|
||||||
|
--
|
||||||
|
-- We relax these rows so:
|
||||||
|
-- * SchoolMask=16 (FROST) gate by Frost school at the proc engine
|
||||||
|
-- * SpellTypeMask=1 (DAMAGE) only damage events trigger CheckProc
|
||||||
|
-- * SpellPhaseMask=2 (HIT) post-hit phase
|
||||||
|
-- * AttributesMask=2 (TRIGGERED) triggered chill payloads still proc
|
||||||
|
-- * SpellFamilyName=0 wildcard -- IsAffected short-circuits to true
|
||||||
|
-- * SpellFamilyMask{0,1,2}=0 wildcard -- no flag-bit gating at this layer
|
||||||
|
--
|
||||||
|
-- Real filtering moves into the Mage AuraScripts:
|
||||||
|
--
|
||||||
|
-- spell_mage_fingers_of_frost_talent attached to 44543 / 44545
|
||||||
|
-- stock Mage : SpellFamilyName=MAGE AND original Mask0 0x100120 / Mask1 0x1000
|
||||||
|
-- Paragon : accept (FROST + DAMAGE gate already enforced)
|
||||||
|
--
|
||||||
|
-- spell_mage_frostbite attached to 11071 / 12496 / 12497
|
||||||
|
-- stock Mage : SpellFamilyName=MAGE AND original Mage Frost-slow Mask
|
||||||
|
-- (Frostbolt / Frost Nova / Cone of Cold / Blizzard / FFB)
|
||||||
|
-- Paragon : accept iff proc spell applies SPELL_AURA_MOD_DECREASE_SPEED
|
||||||
|
-- OR the Paragon already has a slow on the proc target
|
||||||
|
-- (covers the Improved-Blizzard-style "chill via separate
|
||||||
|
-- triggered aura" cross-class case)
|
||||||
|
--
|
||||||
|
-- Net effect: Paragon characters with these talents now have FoF / Frostbite
|
||||||
|
-- proc off cross-class Frost-school chill effects; stock Mages are unchanged.
|
||||||
|
|
||||||
|
DELETE FROM `spell_proc` WHERE `SpellId` IN (44543, 44545, 11071, 12496, 12497);
|
||||||
|
|
||||||
|
INSERT INTO `spell_proc`
|
||||||
|
(`SpellId`, `SchoolMask`, `SpellFamilyName`,
|
||||||
|
`SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`,
|
||||||
|
`ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`,
|
||||||
|
`AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`,
|
||||||
|
`Chance`, `Cooldown`, `Charges`)
|
||||||
|
VALUES
|
||||||
|
-- Fingers of Frost talent ranks (Chance 7% / 15% preserved from stock row).
|
||||||
|
(44543, 16, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 7, 0, 0),
|
||||||
|
(44545, 16, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 15, 0, 0),
|
||||||
|
-- Frostbite talent ranks (5% / 10% / 15% per rank, leave Chance=0 to use
|
||||||
|
-- the DBC ProcChance which already encodes the per-rank percentage).
|
||||||
|
(11071, 16, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 0, 0, 0),
|
||||||
|
(12496, 16, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 0, 0, 0),
|
||||||
|
(12497, 16, 0, 0, 0, 0, 0, 1, 2, 0, 2, 0, 0, 0, 0, 0);
|
||||||
|
|
||||||
|
-- Bind the new AuraScripts (defined in src/server/scripts/Spells/spell_mage.cpp).
|
||||||
|
DELETE FROM `spell_script_names`
|
||||||
|
WHERE `spell_id` IN (44543, 44545, 11071, 12496, 12497)
|
||||||
|
AND `ScriptName` IN ('spell_mage_fingers_of_frost_talent', 'spell_mage_frostbite');
|
||||||
|
|
||||||
|
INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
|
||||||
|
(44543, 'spell_mage_fingers_of_frost_talent'),
|
||||||
|
(44545, 'spell_mage_fingers_of_frost_talent'),
|
||||||
|
(11071, 'spell_mage_frostbite'),
|
||||||
|
(12496, 'spell_mage_frostbite'),
|
||||||
|
(12497, 'spell_mage_frostbite');
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
-- mod-paragon: Maelstrom Weapon (53817) spell_proc fixup.
|
||||||
|
--
|
||||||
|
-- The previous migration (2026_05_11_02.sql) had two bugs in the rewritten
|
||||||
|
-- spell_proc row that prevented stack consumption from firing at all -- not
|
||||||
|
-- just for our new Mage targets, but also for the stock Shaman cast-time
|
||||||
|
-- spells (Lightning Bolt, Chain Lightning, Lesser Healing Wave, etc.).
|
||||||
|
--
|
||||||
|
-- Bugs:
|
||||||
|
--
|
||||||
|
-- * SpellPhaseMask was set to 8. The valid values for SpellPhaseMask are
|
||||||
|
-- PROC_SPELL_PHASE_CAST = 1
|
||||||
|
-- PROC_SPELL_PHASE_HIT = 2
|
||||||
|
-- PROC_SPELL_PHASE_FINISH = 4
|
||||||
|
-- (see SpellMgr.h). Anything else, including 8, never matches a real
|
||||||
|
-- proc event, so the proc engine silently dropped every event before it
|
||||||
|
-- reached the AuraScript. The original stock row uses 1 (CAST), which
|
||||||
|
-- is what fires when the cast packet's setup phase completes -- exactly
|
||||||
|
-- when we want the spellmod-affected cast to consume the buff.
|
||||||
|
--
|
||||||
|
-- * AttributesMask was set to 0. The original stock row uses 8
|
||||||
|
-- PROC_ATTR_REQ_SPELLMOD = 0x8
|
||||||
|
-- which says "only proc on spells that were affected by one of this
|
||||||
|
-- aura's spellmods". This is the bridge between IsAffectedBySpellMod
|
||||||
|
-- (which records the aura into Spell::m_appliedMods when calculating
|
||||||
|
-- cast time / cost) and the proc system (which then knows that the cast
|
||||||
|
-- used the buff and should consume a charge). Without this attribute,
|
||||||
|
-- the proc would either fire too aggressively or not at all depending
|
||||||
|
-- on subsequent gating, but in practice the engine relies on it to
|
||||||
|
-- correlate spellmod use with stack consumption.
|
||||||
|
--
|
||||||
|
-- Fixed row keeps the same family/mask wildcards from the previous
|
||||||
|
-- migration (so the Paragon Mage allowlist in spell_sha_maelstrom_weapon's
|
||||||
|
-- CheckProc still gets the chance to filter), restores SpellPhaseMask=1
|
||||||
|
-- (CAST) and AttributesMask=8 (REQ_SPELLMOD) to match stock semantics, and
|
||||||
|
-- resets SpellTypeMask to 0 (any spell type -- the REQ_SPELLMOD attribute
|
||||||
|
-- already gates by "was the buff actually used", so an extra DAMAGE filter
|
||||||
|
-- is redundant and would block e.g. Lesser Healing Wave on stock Shamans).
|
||||||
|
--
|
||||||
|
-- This restores stock Shaman behavior byte-identically and lets Paragon
|
||||||
|
-- Mage casts also consume stacks via the IsAffectedBySpellMod allowlist.
|
||||||
|
|
||||||
|
DELETE FROM `spell_proc` WHERE `SpellId` = 53817;
|
||||||
|
|
||||||
|
INSERT INTO `spell_proc`
|
||||||
|
(`SpellId`, `SchoolMask`, `SpellFamilyName`,
|
||||||
|
`SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`,
|
||||||
|
`ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`,
|
||||||
|
`AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`,
|
||||||
|
`Chance`, `Cooldown`, `Charges`)
|
||||||
|
VALUES
|
||||||
|
(53817, 0, 0, 0, 0, 0, 0, 0, 1, 0, 8, 0, 0, 0, 0, 0);
|
||||||
@@ -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)
|
||||||
+21
@@ -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);
|
||||||
+15
@@ -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);
|
||||||
+21
@@ -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);
|
||||||
+9
@@ -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');
|
||||||
+8
@@ -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;
|
||||||
File diff suppressed because it is too large
Load Diff
Executable
+37
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Kill AzerothCore authserver + worldserver tmux sessions and any stray processes.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/kill-azeroth-servers.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
AUTH_SESSION="authserver"
|
||||||
|
WORLD_SESSION="worldserver"
|
||||||
|
|
||||||
|
echo "Stopping servers..."
|
||||||
|
|
||||||
|
# Kill tmux sessions
|
||||||
|
if tmux has-session -t "$WORLD_SESSION" 2>/dev/null; then
|
||||||
|
tmux kill-session -t "$WORLD_SESSION"
|
||||||
|
echo " killed tmux session: $WORLD_SESSION"
|
||||||
|
else
|
||||||
|
echo " no tmux session: $WORLD_SESSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if tmux has-session -t "$AUTH_SESSION" 2>/dev/null; then
|
||||||
|
tmux kill-session -t "$AUTH_SESSION"
|
||||||
|
echo " killed tmux session: $AUTH_SESSION"
|
||||||
|
else
|
||||||
|
echo " no tmux session: $AUTH_SESSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up any stray processes not managed by tmux
|
||||||
|
if pkill -x worldserver 2>/dev/null; then
|
||||||
|
echo " killed stray worldserver process"
|
||||||
|
fi
|
||||||
|
if pkill -x authserver 2>/dev/null; then
|
||||||
|
echo " killed stray authserver process"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
@@ -1,23 +1,37 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Start AzerothCore authserver + worldserver detached from the SSH session (nohup + disown).
|
# Start AzerothCore authserver + worldserver in named tmux sessions.
|
||||||
# Stops any already-running authserver/worldserver processes first.
|
# Kills any already-running sessions first (acts as a restart).
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# sudo bash scripts/start-azeroth-servers.sh
|
# bash scripts/start-azeroth-servers.sh
|
||||||
# AZEROTH_BIN=/path/to/azeroth-server/bin bash scripts/start-azeroth-servers.sh
|
# AZEROTH_BIN=/path/to/bin bash scripts/start-azeroth-servers.sh
|
||||||
#
|
#
|
||||||
# Environment:
|
# 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)
|
# AZEROTH_LOG_DIR — log directory (default: <parent of bin>/logs)
|
||||||
|
#
|
||||||
|
# tmux sessions:
|
||||||
|
# authserver — authserver console
|
||||||
|
# worldserver — worldserver console
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
BIN_DIR="${AZEROTH_BIN:-/root/azeroth-server/bin}"
|
BIN_DIR="${AZEROTH_BIN:-/home/fractured-panel/azeroth-server/bin}"
|
||||||
LOG_DIR="${AZEROTH_LOG_DIR:-$(cd "$(dirname "$BIN_DIR")" && pwd)/logs}"
|
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"
|
AUTH_BIN="${BIN_DIR}/authserver"
|
||||||
WORLD_BIN="${BIN_DIR}/worldserver"
|
WORLD_BIN="${BIN_DIR}/worldserver"
|
||||||
|
|
||||||
|
AUTH_SESSION="authserver"
|
||||||
|
WORLD_SESSION="worldserver"
|
||||||
|
|
||||||
|
if ! command -v tmux &>/dev/null; then
|
||||||
|
echo "error: tmux is not installed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ! -x "$AUTH_BIN" ]]; then
|
if [[ ! -x "$AUTH_BIN" ]]; then
|
||||||
echo "error: not found or not executable: $AUTH_BIN" >&2
|
echo "error: not found or not executable: $AUTH_BIN" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -27,23 +41,31 @@ if [[ ! -x "$WORLD_BIN" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
# Tear down existing sessions (ignore errors if they don't exist)
|
||||||
|
tmux kill-session -t "$AUTH_SESSION" 2>/dev/null || true
|
||||||
|
tmux kill-session -t "$WORLD_SESSION" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Also kill any stray processes not managed by tmux
|
||||||
pkill -x authserver 2>/dev/null || true
|
pkill -x authserver 2>/dev/null || true
|
||||||
pkill -x worldserver 2>/dev/null || true
|
pkill -x worldserver 2>/dev/null || true
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
mkdir -p "$LOG_DIR"
|
# Launch authserver in a tmux session
|
||||||
|
tmux new-session -d -s "$AUTH_SESSION" -c "$BIN_DIR" \
|
||||||
cd "$BIN_DIR"
|
"$AUTH_BIN -c ${CONF_DIR}/authserver.conf 2>&1 | tee -a ${LOG_DIR}/authserver.log"
|
||||||
|
|
||||||
nohup "$AUTH_BIN" >>"$LOG_DIR/authserver.log" 2>&1 &
|
|
||||||
disown || true
|
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
nohup "$WORLD_BIN" >>"$LOG_DIR/worldserver.log" 2>&1 &
|
# Launch worldserver in a tmux session
|
||||||
disown || true
|
tmux new-session -d -s "$WORLD_SESSION" -c "$BIN_DIR" \
|
||||||
|
"$WORLD_BIN -c ${CONF_DIR}/worldserver.conf 2>&1 | tee -a ${LOG_DIR}/worldserver.log"
|
||||||
|
|
||||||
echo "Started authserver and worldserver (survives SSH disconnect)."
|
echo "Started servers in tmux sessions."
|
||||||
|
echo " tmux attach -t $AUTH_SESSION — authserver console"
|
||||||
|
echo " tmux attach -t $WORLD_SESSION — worldserver console"
|
||||||
echo "Bin: $BIN_DIR"
|
echo "Bin: $BIN_DIR"
|
||||||
|
echo "Config: $CONF_DIR"
|
||||||
echo "Logs: $LOG_DIR/authserver.log"
|
echo "Logs: $LOG_DIR/authserver.log"
|
||||||
echo " $LOG_DIR/worldserver.log"
|
echo " $LOG_DIR/worldserver.log"
|
||||||
|
|||||||
@@ -6,24 +6,25 @@
|
|||||||
# (see docs/DEPLOY_LINUX_VPS.md).
|
# (see docs/DEPLOY_LINUX_VPS.md).
|
||||||
#
|
#
|
||||||
# What this does:
|
# What this does:
|
||||||
# 1. git pull on the current branch (optional; can skip)
|
# 1. Optionally kill running servers (tmux sessions)
|
||||||
# 2. ./acore.sh compiler build — or compiler all for a full clean rebuild
|
# 2. git pull on the current branch (optional; can skip)
|
||||||
|
# 3. ./acore.sh compiler build — or compiler all for a full clean rebuild
|
||||||
|
# 4. Optionally restart servers in tmux sessions
|
||||||
#
|
#
|
||||||
# Database migrations from data/sql/updates/ run when you next start worldserver/authserver
|
# Database migrations from data/sql/updates/ run when you next start worldserver/authserver
|
||||||
# (Updates.* / SourceDirectory in *.conf). This script does not start or stop daemons unless
|
# (Updates.* / SourceDirectory in *.conf).
|
||||||
# you pass --run-after or set FRACTURED_POST_UPDATE_CMD.
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# bash scripts/vps-update-server.sh
|
# bash scripts/vps-update-server.sh # pull + compile only
|
||||||
# bash scripts/vps-update-server.sh --full
|
# bash scripts/vps-update-server.sh --restart # pull + compile + restart servers in tmux
|
||||||
# bash scripts/vps-update-server.sh --no-pull
|
# bash scripts/vps-update-server.sh --full --restart # clean rebuild + restart
|
||||||
|
# bash scripts/vps-update-server.sh --no-pull --restart # compile current tree + restart
|
||||||
# bash scripts/vps-update-server.sh --dry-run
|
# bash scripts/vps-update-server.sh --dry-run
|
||||||
# FRACTURED_POST_UPDATE_CMD='sudo systemctl restart fractured-world' bash scripts/vps-update-server.sh --run-after
|
# bash scripts/vps-update-server.sh --run-after 'custom command here'
|
||||||
# bash scripts/vps-update-server.sh --run-after 'sudo systemctl restart fractured-world'
|
|
||||||
#
|
#
|
||||||
# Environment:
|
# Environment:
|
||||||
# FRACTURED_GIT_REMOTE — remote name (default: origin)
|
# FRACTURED_GIT_REMOTE — remote name (default: origin)
|
||||||
# FRACTURED_POST_UPDATE_CMD — shell command run after a successful compile (if --run-after is passed without an argument, this is used)
|
# FRACTURED_POST_UPDATE_CMD — shell command run after compile (used by bare --run-after)
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -35,6 +36,8 @@ FULL_BUILD=0
|
|||||||
COMPILE_ONLY=0
|
COMPILE_ONLY=0
|
||||||
DRY_RUN=0
|
DRY_RUN=0
|
||||||
DO_RUN_AFTER=0
|
DO_RUN_AFTER=0
|
||||||
|
DO_RESTART=0
|
||||||
|
INSTALL_PREFIX=""
|
||||||
POST_UPDATE_CMD="${FRACTURED_POST_UPDATE_CMD:-}"
|
POST_UPDATE_CMD="${FRACTURED_POST_UPDATE_CMD:-}"
|
||||||
GIT_REMOTE="${FRACTURED_GIT_REMOTE:-origin}"
|
GIT_REMOTE="${FRACTURED_GIT_REMOTE:-origin}"
|
||||||
|
|
||||||
@@ -49,9 +52,11 @@ Options:
|
|||||||
--no-pull Skip git pull (only compile current tree).
|
--no-pull Skip git pull (only compile current tree).
|
||||||
--full ./acore.sh compiler all (clean + configure + compile).
|
--full ./acore.sh compiler all (clean + configure + compile).
|
||||||
--compile-only ./acore.sh compiler compile (incremental).
|
--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.
|
--dry-run Print commands without running them.
|
||||||
--run-after [CMD] Run shell command after successful compile. If CMD is omitted,
|
--restart Kill servers before compile, restart in tmux after.
|
||||||
uses FRACTURED_POST_UPDATE_CMD from the environment.
|
--run-after [CMD] Run a custom shell command after successful compile.
|
||||||
|
If CMD is omitted, uses FRACTURED_POST_UPDATE_CMD.
|
||||||
|
|
||||||
Environment:
|
Environment:
|
||||||
FRACTURED_GIT_REMOTE Git remote (default: origin).
|
FRACTURED_GIT_REMOTE Git remote (default: origin).
|
||||||
@@ -87,10 +92,23 @@ while [[ $# -gt 0 ]]; do
|
|||||||
COMPILE_ONLY=1
|
COMPILE_ONLY=1
|
||||||
shift
|
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)
|
||||||
DRY_RUN=1
|
DRY_RUN=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--restart)
|
||||||
|
DO_RESTART=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
--run-after)
|
--run-after)
|
||||||
DO_RUN_AFTER=1
|
DO_RUN_AFTER=1
|
||||||
shift
|
shift
|
||||||
@@ -129,6 +147,28 @@ fi
|
|||||||
|
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
|
KILL_SCRIPT="${SCRIPT_DIR}/kill-azeroth-servers.sh"
|
||||||
|
START_SCRIPT="${SCRIPT_DIR}/start-azeroth-servers.sh"
|
||||||
|
|
||||||
|
if [[ "$DO_RESTART" -eq 1 ]]; then
|
||||||
|
if [[ ! -f "$KILL_SCRIPT" || ! -f "$START_SCRIPT" ]]; then
|
||||||
|
echo "error: --restart requires kill-azeroth-servers.sh and start-azeroth-servers.sh in scripts/" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> stopping servers before compile"
|
||||||
|
run bash "$KILL_SCRIPT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
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
|
echo "error: --run-after needs a command or FRACTURED_POST_UPDATE_CMD set in the environment." >&2
|
||||||
exit 2
|
exit 2
|
||||||
@@ -168,6 +208,11 @@ else
|
|||||||
run ./acore.sh compiler build
|
run ./acore.sh compiler build
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$DO_RESTART" -eq 1 ]]; then
|
||||||
|
echo "==> restarting servers in tmux sessions"
|
||||||
|
run bash "$START_SCRIPT"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$DO_RUN_AFTER" -eq 1 ]]; then
|
if [[ "$DO_RUN_AFTER" -eq 1 ]]; then
|
||||||
echo "==> post-update: $POST_UPDATE_CMD"
|
echo "==> post-update: $POST_UPDATE_CMD"
|
||||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||||
@@ -178,4 +223,8 @@ if [[ "$DO_RUN_AFTER" -eq 1 ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Done. Restart authserver/worldserver (or your service manager) when ready so new binaries and SQL updates apply."
|
if [[ "$DO_RESTART" -eq 0 && "$DO_RUN_AFTER" -eq 0 ]]; then
|
||||||
|
echo "Done. Run 'bash scripts/start-azeroth-servers.sh' to (re)start servers in tmux."
|
||||||
|
else
|
||||||
|
echo "Done."
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1753,9 +1753,9 @@ InstantLogout = 1
|
|||||||
#
|
#
|
||||||
# PlayerSaveInterval
|
# PlayerSaveInterval
|
||||||
# Description: Time (in milliseconds) for player save interval.
|
# Description: Time (in milliseconds) for player save interval.
|
||||||
# Default: 900000 - (15 min)
|
# Default: 300000 - (5 min)
|
||||||
|
|
||||||
PlayerSaveInterval = 900000
|
PlayerSaveInterval = 300000
|
||||||
|
|
||||||
#
|
#
|
||||||
# PlayerSave.Stats.MinLevel
|
# PlayerSave.Stats.MinLevel
|
||||||
@@ -2260,9 +2260,9 @@ Achievement.RealmFirstKillWindow = 60
|
|||||||
# MaxPrimaryTradeSkill
|
# MaxPrimaryTradeSkill
|
||||||
# Description: Maximum number of primary professions a character can learn.
|
# Description: Maximum number of primary professions a character can learn.
|
||||||
# Range: 0-11
|
# Range: 0-11
|
||||||
# Default: 2
|
# Default: 11 - (All WotLK primary professions; set 2 for retail-like two-slot cap.)
|
||||||
|
|
||||||
MaxPrimaryTradeSkill = 2
|
MaxPrimaryTradeSkill = 11
|
||||||
|
|
||||||
#
|
#
|
||||||
# SkillChance.Prospecting
|
# SkillChance.Prospecting
|
||||||
|
|||||||
@@ -678,6 +678,7 @@ enum RBACPermissions
|
|||||||
RBAC_PERM_COMMAND_BF_QUEUE = 913,
|
RBAC_PERM_COMMAND_BF_QUEUE = 913,
|
||||||
RBAC_PERM_COMMAND_PET_LIST = 914,
|
RBAC_PERM_COMMAND_PET_LIST = 914,
|
||||||
RBAC_PERM_COMMAND_PET_DELETE = 915,
|
RBAC_PERM_COMMAND_PET_DELETE = 915,
|
||||||
|
RBAC_PERM_COMMAND_LEARN_ALL_MOUNTS = 916,
|
||||||
// custom permissions 1000+
|
// custom permissions 1000+
|
||||||
RBAC_PERM_MAX
|
RBAC_PERM_MAX
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3640,6 +3640,13 @@ bool Creature::IsMovementPreventedByCasting() const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fractured: cast-time mount summon (player-style mount spells on NPCs are rare but supported).
|
||||||
|
if (Spell* genSpell = m_currentSpells[CURRENT_GENERIC_SPELL])
|
||||||
|
{
|
||||||
|
if (genSpell->getState() == SPELL_STATE_PREPARING && genSpell->m_spellInfo->IsCastTimeRidingMountSpell())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (HasSpellFocus())
|
if (HasSpellFocus())
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -7143,6 +7143,18 @@ bool Player::CheckAttackFitToAuraRequirement(WeaponAttackType attackType, AuraEf
|
|||||||
if (spellInfo->EquippedItemClass == -1)
|
if (spellInfo->EquippedItemClass == -1)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
// 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);
|
Item* item = GetWeaponForAttack(attackType, true);
|
||||||
if (!item || !item->IsFitToSpellRequirements(spellInfo))
|
if (!item || !item->IsFitToSpellRequirements(spellInfo))
|
||||||
return false;
|
return false;
|
||||||
@@ -7208,7 +7220,7 @@ void Player::ApplyEquipSpell(SpellInfo const* spellInfo, Item* item, bool apply,
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Cannot be used in this stance/form
|
// Cannot be used in this stance/form
|
||||||
if (spellInfo->CheckShapeshift(GetShapeshiftForm()) != SPELL_CAST_OK)
|
if (spellInfo->CheckShapeshift(GetShapeshiftForm(), this) != SPELL_CAST_OK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (form_change) // check aura active state from other form
|
if (form_change) // check aura active state from other form
|
||||||
@@ -7228,7 +7240,7 @@ void Player::ApplyEquipSpell(SpellInfo const* spellInfo, Item* item, bool apply,
|
|||||||
if (form_change) // check aura compatibility
|
if (form_change) // check aura compatibility
|
||||||
{
|
{
|
||||||
// Cannot be used in this stance/form
|
// Cannot be used in this stance/form
|
||||||
if (spellInfo->CheckShapeshift(GetShapeshiftForm()) == SPELL_CAST_OK)
|
if (spellInfo->CheckShapeshift(GetShapeshiftForm(), this) == SPELL_CAST_OK)
|
||||||
return; // and remove only not compatible at form change
|
return; // and remove only not compatible at form change
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12036,7 +12048,20 @@ void Player::learnSkillRewardedSpells(uint32 skill_id, uint32 skill_value)
|
|||||||
// weapon, language, and racial skill cascades stay enabled so things
|
// weapon, language, and racial skill cascades stay enabled so things
|
||||||
// like recipe auto-learn, weapon proficiencies, and racial perks
|
// like recipe auto-learn, weapon proficiencies, and racial perks
|
||||||
// still work.
|
// 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 (SkillLineEntry const* sl = sSkillLineStore.LookupEntry(skill_id))
|
||||||
if (sl->categoryId == SKILL_CATEGORY_CLASS)
|
if (sl->categoryId == SKILL_CATEGORY_CLASS)
|
||||||
@@ -12581,6 +12606,31 @@ bool Player::HasItemFitToSpellRequirements(SpellInfo const* spellInfo, Item cons
|
|||||||
if (spellInfo->EquippedItemClass < 0)
|
if (spellInfo->EquippedItemClass < 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
if (item != ignoreItem)
|
||||||
|
if (ItemTemplate const* proto = item->GetTemplate())
|
||||||
|
if (proto->Class == ITEM_CLASS_WEAPON)
|
||||||
|
return true;
|
||||||
|
// No weapon equipped at all -- fall through to stock logic, which
|
||||||
|
// returns false for passive talent auras (correct: an unarmed
|
||||||
|
// Paragon shouldn't have weapon talents active).
|
||||||
|
}
|
||||||
|
|
||||||
// scan other equipped items for same requirements (mostly 2 daggers/etc)
|
// scan other equipped items for same requirements (mostly 2 daggers/etc)
|
||||||
// for optimize check 2 used cases only
|
// for optimize check 2 used cases only
|
||||||
switch (spellInfo->EquippedItemClass)
|
switch (spellInfo->EquippedItemClass)
|
||||||
|
|||||||
@@ -1828,6 +1828,7 @@ public:
|
|||||||
uint32 GetLastPotionId() { return m_lastPotionId; }
|
uint32 GetLastPotionId() { return m_lastPotionId; }
|
||||||
void SetLastPotionId(uint32 item_id) { m_lastPotionId = item_id; }
|
void SetLastPotionId(uint32 item_id) { m_lastPotionId = item_id; }
|
||||||
void UpdatePotionCooldown(Spell* spell = nullptr);
|
void UpdatePotionCooldown(Spell* spell = nullptr);
|
||||||
|
void AtEnterCombat() override;
|
||||||
void AtExitCombat() override;
|
void AtExitCombat() override;
|
||||||
|
|
||||||
void setResurrectRequestData(ObjectGuid guid, uint32 mapId, float X, float Y, float Z, uint32 health, uint32 mana)
|
void setResurrectRequestData(ObjectGuid guid, uint32 mapId, float X, float Y, float Z, uint32 health, uint32 mana)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "Player.h"
|
#include "Player.h"
|
||||||
#include "ScriptMgr.h"
|
#include "ScriptMgr.h"
|
||||||
#include "SkillDiscovery.h"
|
#include "SkillDiscovery.h"
|
||||||
|
#include "Spell.h"
|
||||||
#include "SpellAuraEffects.h"
|
#include "SpellAuraEffects.h"
|
||||||
#include "SpellMgr.h"
|
#include "SpellMgr.h"
|
||||||
#include "UpdateFieldFlags.h"
|
#include "UpdateFieldFlags.h"
|
||||||
@@ -332,6 +333,28 @@ void Player::Update(uint32 p_time)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_additionalSaveTimer)
|
||||||
|
{
|
||||||
|
if (p_time >= m_additionalSaveTimer)
|
||||||
|
{
|
||||||
|
m_additionalSaveTimer = 0;
|
||||||
|
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
|
||||||
|
|
||||||
|
if (m_additionalSaveMask & ADDITIONAL_SAVING_INVENTORY_AND_GOLD)
|
||||||
|
SaveInventoryAndGoldToDB(trans);
|
||||||
|
|
||||||
|
if (m_additionalSaveMask & ADDITIONAL_SAVING_QUEST_STATUS)
|
||||||
|
_SaveQuestStatus(trans);
|
||||||
|
|
||||||
|
CharacterDatabase.CommitTransaction(trans);
|
||||||
|
m_additionalSaveMask = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_additionalSaveTimer -= p_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Water/drowning
|
// Handle Water/drowning
|
||||||
HandleDrowning(p_time);
|
HandleDrowning(p_time);
|
||||||
|
|
||||||
@@ -1539,6 +1562,27 @@ void Player::UpdatePvP(bool state, bool _override)
|
|||||||
sScriptMgr->OnPlayerPVPFlagChange(this, state);
|
sScriptMgr->OnPlayerPVPFlagChange(this, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Player::AtEnterCombat()
|
||||||
|
{
|
||||||
|
Unit::AtEnterCombat();
|
||||||
|
|
||||||
|
// Fractured: cancel cast-time mount summon if combat starts mid-cast.
|
||||||
|
for (uint32 spellType = CURRENT_FIRST_NON_MELEE_SPELL; spellType < CURRENT_MAX_SPELL; ++spellType)
|
||||||
|
{
|
||||||
|
if (Spell* spell = GetCurrentSpell(CurrentSpellTypes(spellType)))
|
||||||
|
{
|
||||||
|
if (SpellInfo const* info = spell->GetSpellInfo())
|
||||||
|
{
|
||||||
|
if (info->IsCastTimeRidingMountSpell())
|
||||||
|
{
|
||||||
|
InterruptSpell(CurrentSpellTypes(spellType), false, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Player::AtExitCombat()
|
void Player::AtExitCombat()
|
||||||
{
|
{
|
||||||
Unit::AtExitCombat();
|
Unit::AtExitCombat();
|
||||||
|
|||||||
@@ -474,7 +474,25 @@ void Player::UpdateAttackPowerAndDamage(bool ranged)
|
|||||||
switch (GetShapeshiftForm())
|
switch (GetShapeshiftForm())
|
||||||
{
|
{
|
||||||
case FORM_CAT:
|
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;
|
break;
|
||||||
case FORM_BEAR:
|
case FORM_BEAR:
|
||||||
case FORM_DIREBEAR:
|
case FORM_DIREBEAR:
|
||||||
|
|||||||
@@ -78,6 +78,19 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
|
// Fractured / Paragon: single source of truth for the runtime "is this
|
||||||
|
// caller eligible for the cross-class wildcard?" question. Centralizing
|
||||||
|
// here keeps every dependent behavior (family-name skip in
|
||||||
|
// SpellInfo::IsAffected, PERIODIC_LEECH disease counting in
|
||||||
|
// GetDiseasesByCaster, instant-cast intercept in Spell::prepare for
|
||||||
|
// Predator's / Nature's Swiftness, Vampiric Embrace CheckProc cross-family
|
||||||
|
// path, etc.) flipping in lockstep when the config flag is toggled.
|
||||||
|
bool IsParagonWildcardCaller(Unit const* listener)
|
||||||
|
{
|
||||||
|
return listener && listener->getClass() == CLASS_PARAGON
|
||||||
|
&& sWorld->getBoolConfig(CONFIG_PARAGON_WILDCARD_FAMILY);
|
||||||
|
}
|
||||||
|
|
||||||
// Fractured / Paragon: cross-class wildcard helper used by ad-hoc
|
// Fractured / Paragon: cross-class wildcard helper used by ad-hoc
|
||||||
// `switch (SpellFamilyName)` listener gates in Unit / SpellEffects /
|
// `switch (SpellFamilyName)` listener gates in Unit / SpellEffects /
|
||||||
// SpellAuraEffects. Returns true when the listener is a CLASS_PARAGON
|
// SpellAuraEffects. Returns true when the listener is a CLASS_PARAGON
|
||||||
@@ -85,12 +98,127 @@
|
|||||||
// to strict family-name equality.
|
// to strict family-name equality.
|
||||||
bool ParagonFamilyMatches(Unit const* listener, uint32 expectedFamily, uint32 actualFamily)
|
bool ParagonFamilyMatches(Unit const* listener, uint32 expectedFamily, uint32 actualFamily)
|
||||||
{
|
{
|
||||||
if (listener && listener->getClass() == CLASS_PARAGON
|
if (IsParagonWildcardCaller(listener))
|
||||||
&& sWorld->getBoolConfig(CONFIG_PARAGON_WILDCARD_FAMILY))
|
|
||||||
return true;
|
return true;
|
||||||
return expectedFamily == actualFamily;
|
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] =
|
float baseMoveSpeed[MAX_MOVE_TYPE] =
|
||||||
{
|
{
|
||||||
2.5f, // MOVE_WALK
|
2.5f, // MOVE_WALK
|
||||||
@@ -4375,6 +4503,13 @@ bool Unit::IsMovementPreventedByCasting() const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fractured: cast-time mount summon may be completed while moving.
|
||||||
|
if (Spell* genSpell = m_currentSpells[CURRENT_GENERIC_SPELL])
|
||||||
|
{
|
||||||
|
if (genSpell->getState() == SPELL_STATE_PREPARING && genSpell->m_spellInfo->IsCastTimeRidingMountSpell())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// channeled spells during channel stage (after the initial cast timer) allow movement with a specific spell attribute
|
// channeled spells during channel stage (after the initial cast timer) allow movement with a specific spell attribute
|
||||||
if (Spell* spell = m_currentSpells[CURRENT_CHANNELED_SPELL])
|
if (Spell* spell = m_currentSpells[CURRENT_CHANNELED_SPELL])
|
||||||
{
|
{
|
||||||
@@ -6136,17 +6271,40 @@ AuraEffect* Unit::IsScriptOverriden(SpellInfo const* spell, int32 script) const
|
|||||||
|
|
||||||
uint32 Unit::GetDiseasesByCaster(ObjectGuid casterGUID, uint8 mode)
|
uint32 Unit::GetDiseasesByCaster(ObjectGuid casterGUID, uint8 mode)
|
||||||
{
|
{
|
||||||
static const AuraType diseaseAuraTypes[] =
|
ObjectGuid drwGUID;
|
||||||
|
|
||||||
|
// Fractured / Paragon: when the caller (the unit whose strike is
|
||||||
|
// counting diseases -- e.g. Death Strike heal, Blood Strike / Heart
|
||||||
|
// Strike / Obliterate per-disease damage, Glyph of Scourge Strike
|
||||||
|
// refresh) is a CLASS_PARAGON player AND Paragon.WildcardFamilyMatching
|
||||||
|
// is on, also walk SPELL_AURA_PERIODIC_LEECH. That picks up Priest
|
||||||
|
// Devouring Plague, which uses ApplyAuraName 53 (PERIODIC_LEECH) instead
|
||||||
|
// of 3 (PERIODIC_DAMAGE) and is therefore invisible to the stock loop
|
||||||
|
// even though its Dispel field is DISPEL_DISEASE. A full Spell.dbc scan
|
||||||
|
// confirms Devouring Plague is the ONLY entry that satisfies both
|
||||||
|
// `Dispel == DISPEL_DISEASE` and a leech periodic effect, so this does
|
||||||
|
// not accidentally drag any other spell into the disease pool. Stock
|
||||||
|
// (non-Paragon) callers fall through to the original 2-entry iteration
|
||||||
|
// and observe identical behavior.
|
||||||
|
bool paragonWildcardLeech = false;
|
||||||
|
if (Player* playerCaster = ObjectAccessor::GetPlayer(*this, casterGUID))
|
||||||
|
{
|
||||||
|
drwGUID = playerCaster->getRuneWeaponGUID();
|
||||||
|
paragonWildcardLeech = IsParagonWildcardCaller(playerCaster);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuraType diseaseAuraTypes[4] =
|
||||||
{
|
{
|
||||||
SPELL_AURA_PERIODIC_DAMAGE, // Frost Fever and Blood Plague
|
SPELL_AURA_PERIODIC_DAMAGE, // Frost Fever and Blood Plague
|
||||||
SPELL_AURA_LINKED, // Crypt Fever and Ebon Plague
|
SPELL_AURA_LINKED, // Crypt Fever and Ebon Plague
|
||||||
|
SPELL_AURA_NONE,
|
||||||
SPELL_AURA_NONE
|
SPELL_AURA_NONE
|
||||||
};
|
};
|
||||||
|
if (paragonWildcardLeech)
|
||||||
ObjectGuid drwGUID;
|
{
|
||||||
|
diseaseAuraTypes[2] = SPELL_AURA_PERIODIC_LEECH; // Priest Devouring Plague (Paragon-only)
|
||||||
if (Player* playerCaster = ObjectAccessor::GetPlayer(*this, casterGUID))
|
diseaseAuraTypes[3] = SPELL_AURA_NONE;
|
||||||
drwGUID = playerCaster->getRuneWeaponGUID();
|
}
|
||||||
|
|
||||||
uint32 diseases = 0;
|
uint32 diseases = 0;
|
||||||
for (uint8 index = 0; diseaseAuraTypes[index] != SPELL_AURA_NONE; ++index)
|
for (uint8 index = 0; diseaseAuraTypes[index] != SPELL_AURA_NONE; ++index)
|
||||||
|
|||||||
@@ -2268,6 +2268,16 @@ private:
|
|||||||
ValuesUpdateCache _valuesUpdateCache;
|
ValuesUpdateCache _valuesUpdateCache;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fractured / Paragon: returns true iff `listener` is a CLASS_PARAGON player
|
||||||
|
// AND `Paragon.WildcardFamilyMatching` is enabled. Single source of truth for
|
||||||
|
// the gate that controls every cross-class wildcard path (family-name skip in
|
||||||
|
// SpellInfo::IsAffected, leech-aura disease counting in
|
||||||
|
// Unit::GetDiseasesByCaster, the cross-school instant-cast intercept in
|
||||||
|
// Spell::prepare for Predator's / Nature's Swiftness, the Vampiric Embrace
|
||||||
|
// CheckProc cross-family path, etc.). Centralizing the check means runtime
|
||||||
|
// kill-switching the wildcard config flips every behavior together.
|
||||||
|
[[nodiscard]] bool IsParagonWildcardCaller(Unit const* listener);
|
||||||
|
|
||||||
// Fractured / Paragon: helper for ad-hoc `switch (SpellFamilyName)` listener
|
// Fractured / Paragon: helper for ad-hoc `switch (SpellFamilyName)` listener
|
||||||
// gates scattered across Unit / SpellEffects / SpellAuraEffects. When the
|
// gates scattered across Unit / SpellEffects / SpellAuraEffects. When the
|
||||||
// listener (i.e. the unit holding the gating talent / aura) is a Paragon
|
// listener (i.e. the unit holding the gating talent / aura) is a Paragon
|
||||||
@@ -2277,6 +2287,47 @@ private:
|
|||||||
// beyond what they already include via Unit.h's transitive headers.
|
// beyond what they already include via Unit.h's transitive headers.
|
||||||
[[nodiscard]] bool ParagonFamilyMatches(Unit const* listener, uint32 expectedFamily, uint32 actualFamily);
|
[[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
|
namespace Acore
|
||||||
{
|
{
|
||||||
// Binary predicate for sorting Units based on percent value of a power
|
// Binary predicate for sorting Units based on percent value of a power
|
||||||
|
|||||||
@@ -99,7 +99,13 @@ enum ShapeshiftForm
|
|||||||
FORM_FLIGHT = 0x1D,
|
FORM_FLIGHT = 0x1D,
|
||||||
FORM_STEALTH = 0x1E,
|
FORM_STEALTH = 0x1E,
|
||||||
FORM_MOONKIN = 0x1F,
|
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
|
enum ShapeshiftFlags
|
||||||
|
|||||||
@@ -1373,12 +1373,15 @@ void AuraEffect::HandleShapeshiftBoosts(Unit* target, bool apply) const
|
|||||||
HotWSpellId = 24899;
|
HotWSpellId = 24899;
|
||||||
break;
|
break;
|
||||||
case FORM_BATTLESTANCE:
|
case FORM_BATTLESTANCE:
|
||||||
|
case FORM_PARAGON_BATTLE_STANCE:
|
||||||
spellId = 21156;
|
spellId = 21156;
|
||||||
break;
|
break;
|
||||||
case FORM_DEFENSIVESTANCE:
|
case FORM_DEFENSIVESTANCE:
|
||||||
|
case FORM_PARAGON_DEFENSIVE_STANCE:
|
||||||
spellId = 7376;
|
spellId = 7376;
|
||||||
break;
|
break;
|
||||||
case FORM_BERSERKERSTANCE:
|
case FORM_BERSERKERSTANCE:
|
||||||
|
case FORM_PARAGON_BERSERKER_STANCE:
|
||||||
spellId = 7381;
|
spellId = 7381;
|
||||||
break;
|
break;
|
||||||
case FORM_MOONKIN:
|
case FORM_MOONKIN:
|
||||||
@@ -1545,7 +1548,21 @@ void AuraEffect::HandleShapeshiftBoosts(Unit* target, bool apply) const
|
|||||||
// Master Shapeshifter - Cat
|
// Master Shapeshifter - Cat
|
||||||
if (AuraEffect const* aurEff = target->GetDummyAuraEffect(SPELLFAMILY_GENERIC, 2851, 0))
|
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);
|
target->CastCustomSpell(target, 48420, &bp, nullptr, nullptr, true);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1630,7 +1647,7 @@ void AuraEffect::HandleShapeshiftBoosts(Unit* target, bool apply) const
|
|||||||
|
|
||||||
// Xinef: Remove autoattack spells
|
// Xinef: Remove autoattack spells
|
||||||
if (Spell* spell = target->GetCurrentSpell(CURRENT_MELEE_SPELL))
|
if (Spell* spell = target->GetCurrentSpell(CURRENT_MELEE_SPELL))
|
||||||
if (spell->GetSpellInfo()->CheckShapeshift(newAura ? newAura->GetMiscValue() : 0) != SPELL_CAST_OK)
|
if (spell->GetSpellInfo()->CheckShapeshift(newAura ? newAura->GetMiscValue() : 0, target) != SPELL_CAST_OK)
|
||||||
spell->cancel(true);
|
spell->cancel(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1981,7 +1998,7 @@ void AuraEffect::HandlePhase(AuraApplication const* aurApp, uint8 mode, bool app
|
|||||||
/*** UNIT MODEL ***/
|
/*** 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))
|
if (!(mode & AURA_EFFECT_HANDLE_REAL_OR_REAPPLY_MASK))
|
||||||
return;
|
return;
|
||||||
@@ -1990,8 +2007,6 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
|
|||||||
|
|
||||||
uint32 modelid = 0;
|
uint32 modelid = 0;
|
||||||
Powers PowerType = POWER_MANA;
|
Powers PowerType = POWER_MANA;
|
||||||
ShapeshiftForm form = ShapeshiftForm(GetMiscValue());
|
|
||||||
|
|
||||||
switch (form)
|
switch (form)
|
||||||
{
|
{
|
||||||
case FORM_CAT: // 0x01
|
case FORM_CAT: // 0x01
|
||||||
@@ -2005,6 +2020,9 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
|
|||||||
case FORM_BATTLESTANCE: // 0x11
|
case FORM_BATTLESTANCE: // 0x11
|
||||||
case FORM_DEFENSIVESTANCE: // 0x12
|
case FORM_DEFENSIVESTANCE: // 0x12
|
||||||
case FORM_BERSERKERSTANCE: // 0x13
|
case FORM_BERSERKERSTANCE: // 0x13
|
||||||
|
case FORM_PARAGON_BATTLE_STANCE:
|
||||||
|
case FORM_PARAGON_DEFENSIVE_STANCE:
|
||||||
|
case FORM_PARAGON_BERSERKER_STANCE:
|
||||||
PowerType = POWER_RAGE;
|
PowerType = POWER_RAGE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -2035,10 +2053,10 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
|
|||||||
case FORM_SPIRITOFREDEMPTION: // 0x20
|
case FORM_SPIRITOFREDEMPTION: // 0x20
|
||||||
break;
|
break;
|
||||||
default:
|
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)
|
if (apply)
|
||||||
{
|
{
|
||||||
@@ -2073,8 +2091,8 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
|
|||||||
|
|
||||||
// remove other shapeshift before applying a new one
|
// remove other shapeshift before applying a new one
|
||||||
// xinef: rogue shouldnt be wrapped by this check (shadow dance vs stealth)
|
// xinef: rogue shouldnt be wrapped by this check (shadow dance vs stealth)
|
||||||
if (GetSpellInfo()->SpellFamilyName != SPELLFAMILY_ROGUE)
|
if (aurEff->GetSpellInfo()->SpellFamilyName != SPELLFAMILY_ROGUE)
|
||||||
target->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT, ObjectGuid::Empty, GetBase());
|
target->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT, ObjectGuid::Empty, aurEff->GetBase());
|
||||||
|
|
||||||
// stop handling the effect if it was removed by linked event
|
// stop handling the effect if it was removed by linked event
|
||||||
if (aurApp->GetRemoveMode())
|
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))
|
if (AuraEffect const* dummy = target->GetDummyAuraEffect(SPELLFAMILY_DRUID, 238, 0))
|
||||||
FurorChance = std::max(dummy->GetAmount(), 0);
|
FurorChance = std::max(dummy->GetAmount(), 0);
|
||||||
|
|
||||||
switch (GetMiscValue())
|
switch (aurEff->GetMiscValue())
|
||||||
{
|
{
|
||||||
case FORM_CAT:
|
case FORM_CAT:
|
||||||
{
|
{
|
||||||
int32 basePoints = int32(std::min(oldPower, FurorChance));
|
int32 basePoints = int32(std::min(oldPower, FurorChance));
|
||||||
target->SetPower(POWER_ENERGY, 0);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case FORM_BEAR:
|
case FORM_BEAR:
|
||||||
@@ -2178,13 +2196,16 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
|
|||||||
case FORM_BATTLESTANCE:
|
case FORM_BATTLESTANCE:
|
||||||
case FORM_DEFENSIVESTANCE:
|
case FORM_DEFENSIVESTANCE:
|
||||||
case FORM_BERSERKERSTANCE:
|
case FORM_BERSERKERSTANCE:
|
||||||
|
case FORM_PARAGON_BATTLE_STANCE:
|
||||||
|
case FORM_PARAGON_DEFENSIVE_STANCE:
|
||||||
|
case FORM_PARAGON_BERSERKER_STANCE:
|
||||||
{
|
{
|
||||||
uint32 Rage_val = 0;
|
uint32 Rage_val = 0;
|
||||||
// Defensive Tactics
|
// Defensive Tactics
|
||||||
if (form == FORM_DEFENSIVESTANCE)
|
if (form == FORM_DEFENSIVESTANCE || form == FORM_PARAGON_DEFENSIVE_STANCE)
|
||||||
{
|
{
|
||||||
if (AuraEffect const* aurEff = target->IsScriptOverriden(m_spellInfo, 831))
|
if (AuraEffect const* scriptEff = target->IsScriptOverriden(aurEff->GetSpellInfo(), 831))
|
||||||
Rage_val += aurEff->GetAmount() * 10;
|
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)
|
// Stance mastery + Tactical mastery (both passive, and last have aura only in defense stance, but need apply at any stance switch)
|
||||||
if (target->IsPlayer())
|
if (target->IsPlayer())
|
||||||
@@ -2224,7 +2245,7 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
|
|||||||
|
|
||||||
// adding/removing linked auras
|
// adding/removing linked auras
|
||||||
// add/remove the shapeshift aura's boosts
|
// add/remove the shapeshift aura's boosts
|
||||||
HandleShapeshiftBoosts(target, apply);
|
aurEff->HandleShapeshiftBoosts(target, apply);
|
||||||
|
|
||||||
if (target->IsPlayer())
|
if (target->IsPlayer())
|
||||||
target->ToPlayer()->InitDataForForm();
|
target->ToPlayer()->InitDataForForm();
|
||||||
@@ -2232,8 +2253,8 @@ void AuraEffect::HandleAuraModShapeshift(AuraApplication const* aurApp, uint8 mo
|
|||||||
if (target->IsClass(CLASS_DRUID, CLASS_CONTEXT_ABILITY))
|
if (target->IsClass(CLASS_DRUID, CLASS_CONTEXT_ABILITY))
|
||||||
{
|
{
|
||||||
// Dash
|
// Dash
|
||||||
if (AuraEffect* aurEff = target->GetAuraEffect(SPELL_AURA_MOD_INCREASE_SPEED, SPELLFAMILY_DRUID, 0, 0, 0x8))
|
if (AuraEffect* dashSpeedEff = target->GetAuraEffect(SPELL_AURA_MOD_INCREASE_SPEED, SPELLFAMILY_DRUID, 0, 0, 0x8))
|
||||||
aurEff->RecalculateAmount();
|
dashSpeedEff->RecalculateAmount();
|
||||||
|
|
||||||
// Disarm handling
|
// Disarm handling
|
||||||
// If druid shifts while being disarmed we need to deal with that since forms aren't affected by disarm
|
// 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())
|
if (target->IsPlayer())
|
||||||
{
|
{
|
||||||
SpellShapeshiftFormEntry const* shapeInfo = sSpellShapeshiftFormStore.LookupEntry(form);
|
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
|
// 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)
|
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
|
void AuraEffect::HandleAuraTransform(AuraApplication const* aurApp, uint8 mode, bool apply) const
|
||||||
{
|
{
|
||||||
if (!(mode & AURA_EFFECT_HANDLE_SEND_FOR_CLIENT_MASK))
|
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();
|
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)
|
if (mode & AURA_EFFECT_HANDLE_REAL)
|
||||||
{
|
{
|
||||||
// pet auras
|
// pet auras
|
||||||
|
|||||||
@@ -1973,6 +1973,31 @@ bool Aura::CanStackWith(Aura const* existingAura) const
|
|||||||
|| (sameCaster && m_spellInfo->IsAuraExclusiveBySpecificPerCasterWith(existingSpellInfo)))
|
|| (sameCaster && m_spellInfo->IsAuraExclusiveBySpecificPerCasterWith(existingSpellInfo)))
|
||||||
return false;
|
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
|
// check spell group stack rules
|
||||||
switch (sSpellMgr->CheckSpellGroupStackRules(m_spellInfo, existingSpellInfo))
|
switch (sSpellMgr->CheckSpellGroupStackRules(m_spellInfo, existingSpellInfo))
|
||||||
{
|
{
|
||||||
@@ -2251,8 +2276,23 @@ uint8 Aura::GetProcEffectMask(AuraApplication* aurApp, ProcEventInfo& eventInfo,
|
|||||||
item = target->ToPlayer()->GetUseableItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND);
|
item = target->ToPlayer()->GetUseableItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!item || item->IsBroken() || !item->IsFitToSpellRequirements(GetSpellInfo()))
|
if (!item || item->IsBroken())
|
||||||
return 0;
|
return 0;
|
||||||
|
if (!item->IsFitToSpellRequirements(GetSpellInfo()))
|
||||||
|
{
|
||||||
|
// 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)
|
||||||
|
&& IsParagonWeaponSubclassWildcardSpell(GetSpellInfo()->Id)))
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
#include "Vehicle.h"
|
#include "Vehicle.h"
|
||||||
#include "World.h"
|
#include "World.h"
|
||||||
#include "WorldPacket.h"
|
#include "WorldPacket.h"
|
||||||
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
/// @todo: this import is not necessary for compilation and marked as unused by the IDE
|
/// @todo: this import is not necessary for compilation and marked as unused by the IDE
|
||||||
@@ -3540,42 +3541,66 @@ SpellCastResult Spell::prepare(SpellCastTargets const* targets, AuraEffect const
|
|||||||
if (m_caster->ToPlayer()->GetCommandStatus(CHEAT_CASTTIME))
|
if (m_caster->ToPlayer()->GetCommandStatus(CHEAT_CASTTIME))
|
||||||
m_casttime = 0;
|
m_casttime = 0;
|
||||||
|
|
||||||
// Fractured / Paragon: cross-class Predator's Swiftness (69369).
|
// Fractured / Paragon: cross-class "next Nature spell becomes instant"
|
||||||
// Stock 3.3.5 only ADD_PCT_MODIFIER's the cast time of Druid-family
|
// intercept for the three buffs that share that semantic in 3.3.5:
|
||||||
// Nature spells via class mask, so a Paragon with the buff cannot
|
//
|
||||||
// instant-cast Shaman Chain Lightning / Lightning Bolt or any other
|
// 69369 - Predator's Swiftness (Cataclysm proc payload triggered by
|
||||||
// non-Druid Nature spell. The tooltip ("next Nature spell with a
|
// our spell_paragon_predatory_strikes; see Paragon_SC.cpp)
|
||||||
// base cast time below 10 sec becomes instant") expects all-Nature
|
// 17116 - Druid Nature's Swiftness
|
||||||
// behavior; honor that here for CLASS_PARAGON. We deliberately do
|
// 16188 - Shaman Nature's Swiftness
|
||||||
// not touch the stock SpellMod path -- real Druids continue to hit
|
//
|
||||||
// the existing class-mask code path unchanged.
|
// All three apply SPELL_AURA_ADD_PCT_MODIFIER on SPELLMOD_CASTING_TIME
|
||||||
|
// gated by a Druid- or Shaman-only SpellClassMask, so a Paragon with the
|
||||||
|
// buff cannot instant-cast a Nature spell from a different family
|
||||||
|
// (e.g. a Druid NS Paragon casting Shaman Chain Lightning, or a Shaman
|
||||||
|
// NS Paragon casting Druid Healing Touch). Tooltip text on all three
|
||||||
|
// promises "next Nature spell with a base cast time below 10 sec becomes
|
||||||
|
// instant"; honor that here for CLASS_PARAGON callers when the wildcard
|
||||||
|
// config is on. The stock SpellMod path is untouched -- real Druids /
|
||||||
|
// Shamans / proc consumers continue to hit the existing class-mask code
|
||||||
|
// path unchanged.
|
||||||
if (Player* paragonCaster = m_caster->ToPlayer())
|
if (Player* paragonCaster = m_caster->ToPlayer())
|
||||||
{
|
{
|
||||||
constexpr uint32 SPELL_PARAGON_PREDATORY_SWIFTNESS = 69369;
|
|
||||||
if (m_casttime > 0
|
if (m_casttime > 0
|
||||||
&& paragonCaster->getClass() == CLASS_PARAGON
|
&& IsParagonWildcardCaller(paragonCaster)
|
||||||
&& (m_spellInfo->SchoolMask & SPELL_SCHOOL_MASK_NATURE)
|
&& (m_spellInfo->SchoolMask & SPELL_SCHOOL_MASK_NATURE)
|
||||||
&& m_spellInfo->CastTimeEntry
|
&& m_spellInfo->CastTimeEntry
|
||||||
&& !m_spellInfo->IsChanneled()
|
&& !m_spellInfo->IsChanneled()
|
||||||
&& !HasTriggeredCastFlag(TRIGGERED_FULL_MASK)
|
&& !HasTriggeredCastFlag(TRIGGERED_FULL_MASK)
|
||||||
&& m_spellInfo->CalcCastTime() < 10 * IN_MILLISECONDS
|
&& m_spellInfo->CalcCastTime() < 10 * IN_MILLISECONDS)
|
||||||
&& paragonCaster->HasAura(SPELL_PARAGON_PREDATORY_SWIFTNESS))
|
|
||||||
{
|
{
|
||||||
m_casttime = 0;
|
static constexpr std::array<uint32, 3> kParagonNatureInstantBuffs =
|
||||||
paragonCaster->RemoveAurasDueToSpell(SPELL_PARAGON_PREDATORY_SWIFTNESS);
|
{
|
||||||
|
69369u, // Predator's Swiftness (Paragon proc payload)
|
||||||
|
17116u, // Druid Nature's Swiftness
|
||||||
|
16188u // Shaman Nature's Swiftness
|
||||||
|
};
|
||||||
|
for (uint32 buffId : kParagonNatureInstantBuffs)
|
||||||
|
{
|
||||||
|
if (paragonCaster->HasAura(buffId))
|
||||||
|
{
|
||||||
|
m_casttime = 0;
|
||||||
|
paragonCaster->RemoveAurasDueToSpell(buffId);
|
||||||
|
break; // consume only one buff per cast
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't allow channeled spells / spells with cast time to be casted while moving
|
// don't allow channeled spells / spells with cast time to be casted while moving
|
||||||
// (even if they are interrupted on moving, spells with almost immediate effect get to have their effect processed before movement interrupter kicks in)
|
// (even if they are interrupted on moving, spells with almost immediate effect get to have their effect processed before movement interrupter kicks in)
|
||||||
|
// Fractured: cast-time mount summons (SPELL_AURA_MOUNTED + non-zero base cast) may be started while moving.
|
||||||
if ((m_spellInfo->IsChanneled() || m_casttime) && m_caster->IsPlayer() && m_caster->isMoving() && m_spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT && !IsTriggered())
|
if ((m_spellInfo->IsChanneled() || m_casttime) && m_caster->IsPlayer() && m_caster->isMoving() && m_spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT && !IsTriggered())
|
||||||
{
|
{
|
||||||
// 1. Has casttime, 2. Or doesn't have flag to allow action during channel
|
if (!m_spellInfo->IsCastTimeRidingMountSpell())
|
||||||
if (m_casttime || !m_spellInfo->IsActionAllowedChannel())
|
|
||||||
{
|
{
|
||||||
SendCastResult(SPELL_FAILED_MOVING);
|
// 1. Has casttime, 2. Or doesn't have flag to allow action during channel
|
||||||
finish(false);
|
if (m_casttime || !m_spellInfo->IsActionAllowedChannel())
|
||||||
return SPELL_FAILED_MOVING;
|
{
|
||||||
|
SendCastResult(SPELL_FAILED_MOVING);
|
||||||
|
finish(false);
|
||||||
|
return SPELL_FAILED_MOVING;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4415,9 +4440,11 @@ void Spell::update(uint32 difftime)
|
|||||||
|
|
||||||
// check if the player caster has moved before the spell finished
|
// check if the player caster has moved before the spell finished
|
||||||
// xinef: added preparing state (real cast, skip channels as they have other flags for this)
|
// xinef: added preparing state (real cast, skip channels as they have other flags for this)
|
||||||
|
// Fractured: cast-time mount summons are allowed to complete while moving.
|
||||||
if ((m_caster->IsPlayer() && m_timer != 0) &&
|
if ((m_caster->IsPlayer() && m_timer != 0) &&
|
||||||
m_caster->isMoving() && (m_spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT) && m_spellState == SPELL_STATE_PREPARING &&
|
m_caster->isMoving() && (m_spellInfo->InterruptFlags & SPELL_INTERRUPT_FLAG_MOVEMENT) && m_spellState == SPELL_STATE_PREPARING &&
|
||||||
(m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK || !m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR)))
|
(m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK || !m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR)) &&
|
||||||
|
!m_spellInfo->IsCastTimeRidingMountSpell())
|
||||||
{
|
{
|
||||||
// don't cancel for melee, autorepeat, triggered and instant spells
|
// don't cancel for melee, autorepeat, triggered and instant spells
|
||||||
if (!IsNextMeleeSwingSpell() && !IsAutoRepeat() && !IsTriggered())
|
if (!IsNextMeleeSwingSpell() && !IsAutoRepeat() && !IsTriggered())
|
||||||
@@ -5748,7 +5775,7 @@ SpellCastResult Spell::CheckCast(bool strict, uint32* /*param1*/, uint32* /*para
|
|||||||
if (checkForm)
|
if (checkForm)
|
||||||
{
|
{
|
||||||
// Cannot be used in this stance/form
|
// Cannot be used in this stance/form
|
||||||
SpellCastResult shapeError = m_spellInfo->CheckShapeshift(m_caster->GetShapeshiftForm());
|
SpellCastResult shapeError = m_spellInfo->CheckShapeshift(m_caster->GetShapeshiftForm(), m_caster);
|
||||||
if (shapeError != SPELL_CAST_OK)
|
if (shapeError != SPELL_CAST_OK)
|
||||||
return shapeError;
|
return shapeError;
|
||||||
|
|
||||||
@@ -5794,6 +5821,10 @@ SpellCastResult Spell::CheckCast(bool strict, uint32* /*param1*/, uint32* /*para
|
|||||||
|
|
||||||
if (reqCombat && m_caster->IsInCombat() && !m_spellInfo->CanBeUsedInCombat())
|
if (reqCombat && m_caster->IsInCombat() && !m_spellInfo->CanBeUsedInCombat())
|
||||||
return SPELL_FAILED_AFFECTING_COMBAT;
|
return SPELL_FAILED_AFFECTING_COMBAT;
|
||||||
|
|
||||||
|
// Fractured: cast-time mount summons cannot be used while in combat (even if DBC omits NOT_IN_COMBAT_ONLY_PEACEFUL).
|
||||||
|
if (reqCombat && m_caster->IsInCombat() && m_spellInfo->IsCastTimeRidingMountSpell())
|
||||||
|
return SPELL_FAILED_AFFECTING_COMBAT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Xinef: exploit protection
|
// Xinef: exploit protection
|
||||||
@@ -5821,10 +5852,14 @@ SpellCastResult Spell::CheckCast(bool strict, uint32* /*param1*/, uint32* /*para
|
|||||||
// (not wand currently autorepeat cast delayed to moving stop anyway in spell update code)
|
// (not wand currently autorepeat cast delayed to moving stop anyway in spell update code)
|
||||||
if (m_caster->IsPlayer() && m_caster->ToPlayer()->isMoving() && !IsTriggered())
|
if (m_caster->IsPlayer() && m_caster->ToPlayer()->isMoving() && !IsTriggered())
|
||||||
{
|
{
|
||||||
// skip stuck spell to allow use it in falling case and apply spell limitations at movement
|
// Fractured: cast-time mount summons may be started while moving.
|
||||||
if ((!m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR) || m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK) &&
|
if (!m_spellInfo->IsCastTimeRidingMountSpell())
|
||||||
(IsAutoRepeat() || (m_spellInfo->AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED) != 0))
|
{
|
||||||
return SPELL_FAILED_MOVING;
|
// skip stuck spell to allow use it in falling case and apply spell limitations at movement
|
||||||
|
if ((!m_caster->HasUnitMovementFlag(MOVEMENTFLAG_FALLING_FAR) || m_spellInfo->Effects[0].Effect != SPELL_EFFECT_STUCK) &&
|
||||||
|
(IsAutoRepeat() || (m_spellInfo->AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED) != 0))
|
||||||
|
return SPELL_FAILED_MOVING;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vehicle* vehicle = m_caster->GetVehicle();
|
Vehicle* vehicle = m_caster->GetVehicle();
|
||||||
@@ -7711,56 +7746,64 @@ SpellCastResult Spell::CheckItems(uint32* param1, uint32* param2)
|
|||||||
switch (pItem->GetTemplate()->SubClass)
|
switch (pItem->GetTemplate()->SubClass)
|
||||||
{
|
{
|
||||||
case ITEM_SUBCLASS_WEAPON_THROWN:
|
case ITEM_SUBCLASS_WEAPON_THROWN:
|
||||||
{
|
{
|
||||||
uint32 ammo = pItem->GetEntry();
|
// Fractured: thrown abilities behave like DK runes -- they
|
||||||
if (!m_caster->ToPlayer()->HasItemCount(ammo))
|
// remain usable even when the player has run out of the
|
||||||
return SPELL_FAILED_NO_AMMO;
|
// 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;
|
break;
|
||||||
|
};
|
||||||
case ITEM_SUBCLASS_WEAPON_GUN:
|
case ITEM_SUBCLASS_WEAPON_GUN:
|
||||||
case ITEM_SUBCLASS_WEAPON_BOW:
|
case ITEM_SUBCLASS_WEAPON_BOW:
|
||||||
case ITEM_SUBCLASS_WEAPON_CROSSBOW:
|
case ITEM_SUBCLASS_WEAPON_CROSSBOW:
|
||||||
{
|
{
|
||||||
uint32 ammo = m_caster->ToPlayer()->GetUInt32Value(PLAYER_AMMO_ID);
|
// Fractured: ranged abilities behave like DK runes -- they
|
||||||
if (!ammo)
|
// remain usable when the player has no ammo loaded or the
|
||||||
{
|
// quiver / pouch is empty. The DPS-bonus path (StatSystem.cpp:
|
||||||
// Requires No Ammo
|
// `weaponMin/MaxDamage += GetAmmoDPS() * attackSpeedMod`)
|
||||||
if (m_caster->HasAura(46699))
|
// reads `m_ammoDPS`, which is 0 when no ammo is loaded and
|
||||||
break; // skip other checks
|
// recomputed via Player::_ApplyAmmoBonuses on equip / stack
|
||||||
|
// exhaustion, so a hunter with an empty bag still casts
|
||||||
return SPELL_FAILED_NO_AMMO;
|
// Steady Shot / Aimed Shot etc. -- they just lose the arrow
|
||||||
}
|
// / bullet DPS contribution.
|
||||||
|
//
|
||||||
ItemTemplate const* ammoProto = sObjectMgr->GetItemTemplate(ammo);
|
// We deliberately do NOT clear PLAYER_AMMO_ID when the bag
|
||||||
if (!ammoProto)
|
// empties. Defense in depth alongside the data-side fix:
|
||||||
return SPELL_FAILED_NO_AMMO;
|
//
|
||||||
|
// * The primary client-side fix lives in Spell.dbc --
|
||||||
if (ammoProto->Class != ITEM_CLASS_PROJECTILE)
|
// SpellInfoCorrections.cpp's "drop EquippedItemClass on
|
||||||
return SPELL_FAILED_NO_AMMO;
|
// hunter shot abilities" block (mirrored client-side by
|
||||||
|
// fractured-tooling/_patch_spell_dbc_hunter_ammo.py)
|
||||||
// check ammo ws. weapon compatibility
|
// sets EquippedItemClass = -1 on every player-castable
|
||||||
switch (pItem->GetTemplate()->SubClass)
|
// hunter shot, which removes the 3.3.5a client's
|
||||||
{
|
// "ranged weapon AND ammo slot non-empty" preflight
|
||||||
case ITEM_SUBCLASS_WEAPON_BOW:
|
// gate entirely. After that, ammo is purely a
|
||||||
case ITEM_SUBCLASS_WEAPON_CROSSBOW:
|
// server-side DPS bonus, never a hard requirement.
|
||||||
if (ammoProto->SubClass != ITEM_SUBCLASS_ARROW)
|
//
|
||||||
return SPELL_FAILED_NO_AMMO;
|
// * Keeping the (now-stale) ammo id in PLAYER_AMMO_ID
|
||||||
break;
|
// field is harmless: TakeAmmo's DestroyItemCount
|
||||||
case ITEM_SUBCLASS_WEAPON_GUN:
|
// silently no-ops when HasItemCount is 0, and
|
||||||
if (ammoProto->SubClass != ITEM_SUBCLASS_BULLET)
|
// _ApplyAmmoBonuses already recomputes m_ammoDPS to 0
|
||||||
return SPELL_FAILED_NO_AMMO;
|
// when the proto can no longer be found / the stack is
|
||||||
break;
|
// empty. So the StatSystem.cpp ammo-DPS path gracefully
|
||||||
default:
|
// degrades to "no bonus" the moment the bag goes empty.
|
||||||
return SPELL_FAILED_NO_AMMO;
|
//
|
||||||
}
|
// * Player un-equipping the ammo via the paper-doll
|
||||||
|
// right-click still routes through RemoveAmmo() and
|
||||||
if (!m_caster->ToPlayer()->HasItemCount(ammo))
|
// zeroes the field -- that is the player's explicit
|
||||||
{
|
// action and we leave it alone.
|
||||||
m_caster->ToPlayer()->SetUInt32Value(PLAYER_AMMO_ID, 0);
|
//
|
||||||
return SPELL_FAILED_NO_AMMO;
|
// 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;
|
break;
|
||||||
|
};
|
||||||
case ITEM_SUBCLASS_WEAPON_WAND:
|
case ITEM_SUBCLASS_WEAPON_WAND:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -7831,6 +7874,36 @@ SpellCastResult Spell::CheckSpellFocus()
|
|||||||
// check spell focus object
|
// check spell focus object
|
||||||
if (m_spellInfo->RequiresSpellFocus)
|
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()));
|
CellCoord p(Acore::ComputeCellCoord(m_caster->GetPositionX(), m_caster->GetPositionY()));
|
||||||
Cell cell(p);
|
Cell cell(p);
|
||||||
|
|
||||||
|
|||||||
@@ -909,6 +909,15 @@ bool SpellInfo::HasAura(AuraType aura) const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SpellInfo::IsCastTimeRidingMountSpell() const
|
||||||
|
{
|
||||||
|
if (IsChanneled())
|
||||||
|
return false;
|
||||||
|
if (!HasAura(SPELL_AURA_MOUNTED))
|
||||||
|
return false;
|
||||||
|
return CalcCastTime(nullptr, nullptr) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
bool SpellInfo::HasAnyAura() const
|
bool SpellInfo::HasAnyAura() const
|
||||||
{
|
{
|
||||||
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
|
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
|
||||||
@@ -1378,7 +1387,43 @@ bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod, Unit const* liste
|
|||||||
if (!sScriptMgr->OnIsAffectedBySpellModCheck(affectSpell, this, mod))
|
if (!sScriptMgr->OnIsAffectedBySpellModCheck(affectSpell, this, mod))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return IsAffected(affectSpell->SpellFamilyName, mod->mask, listenerOwner);
|
if (IsAffected(affectSpell->SpellFamilyName, mod->mask, listenerOwner))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Fractured / Paragon: explicit cross-family allowlist for specific
|
||||||
|
// listener auras whose SpellClassMask cannot otherwise bridge classes.
|
||||||
|
// The standard IsAffected wildcard relaxes SpellFamilyName equality but
|
||||||
|
// still requires SpellClassMask & SpellFamilyFlags to overlap; for these
|
||||||
|
// Paragon-only cross-class enablers the source spells live in different
|
||||||
|
// families with non-overlapping class bits, so we whitelist by mod owner
|
||||||
|
// spell ID + target spell first-rank ID. Stock classes never enter here
|
||||||
|
// because IsParagonWildcardCaller short-circuits on non-Paragon owners.
|
||||||
|
if (IsParagonWildcardCaller(listenerOwner))
|
||||||
|
{
|
||||||
|
switch (mod->spellId)
|
||||||
|
{
|
||||||
|
case 53817: // Shaman: Maelstrom Weapon
|
||||||
|
{
|
||||||
|
// 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*/)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SpellInfo::CanPierceImmuneAura(SpellInfo const* auraSpellInfo) const
|
bool SpellInfo::CanPierceImmuneAura(SpellInfo const* auraSpellInfo) const
|
||||||
@@ -1463,7 +1508,7 @@ bool SpellInfo::IsAuraExclusiveBySpecificPerCasterWith(SpellInfo const* spellInf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SpellCastResult SpellInfo::CheckShapeshift(uint32 form) const
|
SpellCastResult SpellInfo::CheckShapeshift(uint32 form, Unit const* caster /*= nullptr*/) const
|
||||||
{
|
{
|
||||||
// talents that learn spells can have stance requirements that need ignore
|
// talents that learn spells can have stance requirements that need ignore
|
||||||
// (this requirement only for client-side stance show in talent description)
|
// (this requirement only for client-side stance show in talent description)
|
||||||
@@ -1471,6 +1516,38 @@ SpellCastResult SpellInfo::CheckShapeshift(uint32 form) const
|
|||||||
(Effects[0].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[1].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[2].Effect == SPELL_EFFECT_LEARN_SPELL))
|
(Effects[0].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[1].Effect == SPELL_EFFECT_LEARN_SPELL || Effects[2].Effect == SPELL_EFFECT_LEARN_SPELL))
|
||||||
return SPELL_CAST_OK;
|
return SPELL_CAST_OK;
|
||||||
|
|
||||||
|
// Fractured / Paragon: Paragons learn Warrior abilities through Advancement
|
||||||
|
// without picking up Battle/Defensive/Berserker Stance, so stance-gated
|
||||||
|
// Warrior spells (e.g. Whirlwind, Sunder Armor, Shield Slam) would otherwise
|
||||||
|
// be uncastable. Bypass the stance check for Paragon casters on any spell
|
||||||
|
// that has a non-zero Stances bitmask, regardless of SpellFamilyName.
|
||||||
|
//
|
||||||
|
// We previously gated this on SpellFamilyName == SPELLFAMILY_WARRIOR, but a
|
||||||
|
// number of SPELLFAMILY_GENERIC spells (notably the iconic Warrior toolbox
|
||||||
|
// -- Berserker Rage 18499, Sunder Armor 7405 / 11596 / 11597 / 25225 /
|
||||||
|
// 47467, Charge 100 / 6178 / 11578, Pummel 6552 / 6554, Shield Bash 72 /
|
||||||
|
// 1671 / 1672 / 29704, Retaliation 20230, Recklessness 1719, Shield Wall
|
||||||
|
// 871, etc.) carry the Stances bitmask but live under SPELLFAMILY_GENERIC
|
||||||
|
// (family 0). The previous narrower gate let those re-trigger the stance
|
||||||
|
// failure for Paragons. Widening to "any non-zero Stances + Paragon" is
|
||||||
|
// safe because:
|
||||||
|
//
|
||||||
|
// * The bypass returns SPELL_CAST_OK only when IsParagonWildcardCaller
|
||||||
|
// is true -- stock classes never enter this branch.
|
||||||
|
// * Druid form-gated spells (Cat Form / Bear Form / Moonkin / Tree)
|
||||||
|
// still fire the Druid GCD/form rules elsewhere; CheckShapeshift is
|
||||||
|
// about *requiring* a form to cast, which is exactly what we want
|
||||||
|
// to bypass for Paragons (they never picked the form).
|
||||||
|
// * Item enchant scrolls and other shapeshift-marked utility spells
|
||||||
|
// remain unaffected because they aren't in a Paragon's spellbook.
|
||||||
|
if (Stances != 0 && IsParagonWildcardCaller(caster))
|
||||||
|
{
|
||||||
|
LOG_DEBUG("server.scripts",
|
||||||
|
"[paragon-diag] CheckShapeshift bypass: spell={} family={} stances=0x{:x} form={}",
|
||||||
|
Id, SpellFamilyName, Stances, form);
|
||||||
|
return SPELL_CAST_OK;
|
||||||
|
}
|
||||||
|
|
||||||
uint32 stanceMask = (form ? 1 << (form - 1) : 0);
|
uint32 stanceMask = (form ? 1 << (form - 1) : 0);
|
||||||
|
|
||||||
if (stanceMask & StancesNot) // can explicitly not be casted in this stance
|
if (stanceMask & StancesNot) // can explicitly not be casted in this stance
|
||||||
@@ -2079,6 +2156,10 @@ SpellSpecificType SpellInfo::LoadSpellSpecific() const
|
|||||||
{
|
{
|
||||||
case SPELLFAMILY_GENERIC:
|
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)
|
// Food / Drinks (mostly)
|
||||||
if (AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED)
|
if (AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -434,6 +434,9 @@ public:
|
|||||||
bool HasEffect(SpellEffects effect) const;
|
bool HasEffect(SpellEffects effect) const;
|
||||||
bool HasEffectMechanic(Mechanics mechanic) const;
|
bool HasEffectMechanic(Mechanics mechanic) const;
|
||||||
bool HasAura(AuraType aura) const;
|
bool HasAura(AuraType aura) const;
|
||||||
|
/// Summon mount aura (SPELL_AURA_MOUNTED) with a non-zero base cast time from SpellCastTimes.dbc.
|
||||||
|
/// Used by Fractured mount rules: castable while moving, never in combat, interrupted on combat enter.
|
||||||
|
bool IsCastTimeRidingMountSpell() const;
|
||||||
bool HasAnyAura() const;
|
bool HasAnyAura() const;
|
||||||
bool HasAreaAuraEffect() const;
|
bool HasAreaAuraEffect() const;
|
||||||
bool HasOnlyDamageEffects() const;
|
bool HasOnlyDamageEffects() const;
|
||||||
@@ -521,7 +524,7 @@ public:
|
|||||||
bool IsAuraExclusiveBySpecificWith(SpellInfo const* spellInfo) const;
|
bool IsAuraExclusiveBySpecificWith(SpellInfo const* spellInfo) const;
|
||||||
bool IsAuraExclusiveBySpecificPerCasterWith(SpellInfo const* spellInfo) const;
|
bool IsAuraExclusiveBySpecificPerCasterWith(SpellInfo const* spellInfo) const;
|
||||||
|
|
||||||
SpellCastResult CheckShapeshift(uint32 form) const;
|
SpellCastResult CheckShapeshift(uint32 form, Unit const* caster = nullptr) const;
|
||||||
SpellCastResult CheckLocation(uint32 map_id, uint32 zone_id, uint32 area_id, Player* player = nullptr, bool strict = true) const;
|
SpellCastResult CheckLocation(uint32 map_id, uint32 zone_id, uint32 area_id, Player* player = nullptr, bool strict = true) const;
|
||||||
SpellCastResult CheckTarget(Unit const* caster, WorldObject const* target, bool implicit = true) const;
|
SpellCastResult CheckTarget(Unit const* caster, WorldObject const* target, bool implicit = true) const;
|
||||||
SpellCastResult CheckExplicitTarget(Unit const* caster, WorldObject const* target, Item const* itemTarget = nullptr) const;
|
SpellCastResult CheckExplicitTarget(Unit const* caster, WorldObject const* target, Item const* itemTarget = nullptr) const;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "DBCStores.h"
|
#include "DBCStores.h"
|
||||||
#include "DBCStructure.h"
|
#include "DBCStructure.h"
|
||||||
#include "GameGraveyard.h"
|
#include "GameGraveyard.h"
|
||||||
|
#include "ItemTemplate.h"
|
||||||
#include "SpellInfo.h"
|
#include "SpellInfo.h"
|
||||||
#include "SpellMgr.h"
|
#include "SpellMgr.h"
|
||||||
|
|
||||||
@@ -5380,6 +5381,139 @@ void SpellMgr::LoadSpellInfoCorrections()
|
|||||||
spellInfo->Attributes |= SPELL_ATTR0_PASSIVE;
|
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
|
// Fractured: strip reagent requirements from every player-class spell at
|
||||||
// load time. Filtered by SpellFamilyName != 0 so that profession spells
|
// load time. Filtered by SpellFamilyName != 0 so that profession spells
|
||||||
// (cooking, alchemy, enchanting, blacksmithing, jewelcrafting, leatherworking,
|
// (cooking, alchemy, enchanting, blacksmithing, jewelcrafting, leatherworking,
|
||||||
@@ -5418,6 +5552,76 @@ void SpellMgr::LoadSpellInfoCorrections()
|
|||||||
LOG_INFO("server.loading", ">> Fractured: cleared reagents on {} class spells", fixedClassSpells);
|
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", ">> Loading spell dbc data corrections in {} ms", GetMSTimeDiffToNow(oldMSTime));
|
||||||
LOG_INFO("server.loading", " ");
|
LOG_INFO("server.loading", " ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ void WorldConfig::BuildConfigCache()
|
|||||||
SetConfigValue<bool>(CONFIG_ALLOW_PLAYER_COMMANDS, "AllowPlayerCommands", 1);
|
SetConfigValue<bool>(CONFIG_ALLOW_PLAYER_COMMANDS, "AllowPlayerCommands", 1);
|
||||||
SetConfigValue<bool>(CONFIG_PRESERVE_CUSTOM_CHANNELS, "PreserveCustomChannels", false);
|
SetConfigValue<bool>(CONFIG_PRESERVE_CUSTOM_CHANNELS, "PreserveCustomChannels", false);
|
||||||
SetConfigValue<uint32>(CONFIG_PRESERVE_CUSTOM_CHANNEL_DURATION, "PreserveCustomChannelDuration", 14);
|
SetConfigValue<uint32>(CONFIG_PRESERVE_CUSTOM_CHANNEL_DURATION, "PreserveCustomChannelDuration", 14);
|
||||||
SetConfigValue<uint32>(CONFIG_INTERVAL_SAVE, "PlayerSaveInterval", 900000);
|
SetConfigValue<uint32>(CONFIG_INTERVAL_SAVE, "PlayerSaveInterval", 300000);
|
||||||
SetConfigValue<uint32>(CONFIG_INTERVAL_DISCONNECT_TOLERANCE, "DisconnectToleranceInterval", 0);
|
SetConfigValue<uint32>(CONFIG_INTERVAL_DISCONNECT_TOLERANCE, "DisconnectToleranceInterval", 0);
|
||||||
SetConfigValue<bool>(CONFIG_STATS_SAVE_ONLY_ON_LOGOUT, "PlayerSave.Stats.SaveOnlyOnLogout", true);
|
SetConfigValue<bool>(CONFIG_STATS_SAVE_ONLY_ON_LOGOUT, "PlayerSave.Stats.SaveOnlyOnLogout", true);
|
||||||
SetConfigValue<bool>(CONFIG_VALIDATE_SKILL_LEARNED_BY_SPELLS, "ValidateSkillLearnedBySpells", true);
|
SetConfigValue<bool>(CONFIG_VALIDATE_SKILL_LEARNED_BY_SPELLS, "ValidateSkillLearnedBySpells", true);
|
||||||
@@ -270,7 +270,8 @@ void WorldConfig::BuildConfigCache()
|
|||||||
SetConfigValue<uint32>(CONFIG_INSTANCE_RESET_TIME_RELATIVE_TIMESTAMP, "Instance.ResetTimeRelativeTimestamp", 1135814400);
|
SetConfigValue<uint32>(CONFIG_INSTANCE_RESET_TIME_RELATIVE_TIMESTAMP, "Instance.ResetTimeRelativeTimestamp", 1135814400);
|
||||||
SetConfigValue<uint32>(CONFIG_INSTANCE_UNLOAD_DELAY, "Instance.UnloadDelay", 1800000);
|
SetConfigValue<uint32>(CONFIG_INSTANCE_UNLOAD_DELAY, "Instance.UnloadDelay", 1800000);
|
||||||
|
|
||||||
SetConfigValue<uint32>(CONFIG_MAX_PRIMARY_TRADE_SKILL, "MaxPrimaryTradeSkill", 2);
|
// WotLK has 11 primary profession skill lines (gathering + crafting); secondary (Cooking, Fishing, First Aid) are not limited here.
|
||||||
|
SetConfigValue<uint32>(CONFIG_MAX_PRIMARY_TRADE_SKILL, "MaxPrimaryTradeSkill", 11);
|
||||||
SetConfigValue<uint32>(CONFIG_MIN_PETITION_SIGNS, "MinPetitionSigns", 9, ConfigValueCache::Reloadable::Yes, [](uint32 const& value) { return value <= 9; }, "<= 9");
|
SetConfigValue<uint32>(CONFIG_MIN_PETITION_SIGNS, "MinPetitionSigns", 9, ConfigValueCache::Reloadable::Yes, [](uint32 const& value) { return value <= 9; }, "<= 9");
|
||||||
|
|
||||||
SetConfigValue<uint32>(CONFIG_GM_LOGIN_STATE, "GM.LoginState", 2);
|
SetConfigValue<uint32>(CONFIG_GM_LOGIN_STATE, "GM.LoginState", 2);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "CommandScript.h"
|
#include "CommandScript.h"
|
||||||
|
#include "DBCStores.h"
|
||||||
#include "Language.h"
|
#include "Language.h"
|
||||||
#include "ObjectMgr.h"
|
#include "ObjectMgr.h"
|
||||||
#include "Pet.h"
|
#include "Pet.h"
|
||||||
@@ -51,6 +52,7 @@ public:
|
|||||||
{ "default", HandleLearnAllDefaultCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_DEFAULT, Console::No },
|
{ "default", HandleLearnAllDefaultCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_DEFAULT, Console::No },
|
||||||
{ "lang", HandleLearnAllLangCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_LANG, Console::No },
|
{ "lang", HandleLearnAllLangCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_LANG, Console::No },
|
||||||
{ "recipes", HandleLearnAllRecipesCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_RECIPES, Console::No },
|
{ "recipes", HandleLearnAllRecipesCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_RECIPES, Console::No },
|
||||||
|
{ "mounts", HandleLearnAllMountsCommand, rbac::RBAC_PERM_COMMAND_LEARN_ALL_MOUNTS, Console::No },
|
||||||
};
|
};
|
||||||
|
|
||||||
static ChatCommandTable learnCommandTable =
|
static ChatCommandTable learnCommandTable =
|
||||||
@@ -386,6 +388,66 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void SetRidingSkillToMaxForPlayer(Player* player)
|
||||||
|
{
|
||||||
|
SkillRaceClassInfoEntry const* rcInfo = GetSkillRaceClassInfo(SKILL_RIDING, player->getRace(), player->getClass());
|
||||||
|
if (!rcInfo || GetSkillRangeType(rcInfo) != SKILL_RANGE_RANK)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SkillTiersEntry const* tier = sSkillTiersStore.LookupEntry(rcInfo->SkillTierID);
|
||||||
|
if (!tier)
|
||||||
|
return;
|
||||||
|
|
||||||
|
uint8 rank = 0;
|
||||||
|
uint16 maxValue = 0;
|
||||||
|
for (uint8 i = 0; i < MAX_SKILL_STEP; ++i)
|
||||||
|
{
|
||||||
|
if (tier->Value[i] == 0)
|
||||||
|
continue;
|
||||||
|
rank = i + 1;
|
||||||
|
maxValue = tier->Value[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rank || !maxValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
player->SetSkill(SKILL_RIDING, rank, maxValue, maxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool HandleLearnAllMountsCommand(ChatHandler* handler)
|
||||||
|
{
|
||||||
|
Player* target = handler->getSelectedPlayer();
|
||||||
|
if (!target)
|
||||||
|
{
|
||||||
|
handler->SendSysMessage(LANG_PLAYER_NOT_FOUND);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetRidingSkillToMaxForPlayer(target);
|
||||||
|
handler->PSendSysMessage("Set Riding skill to maximum for {}.", handler->GetNameLink(target));
|
||||||
|
|
||||||
|
uint32 learned = 0;
|
||||||
|
for (uint32 i = 0; i < sSpellMgr->GetSpellInfoStoreSize(); ++i)
|
||||||
|
{
|
||||||
|
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(i);
|
||||||
|
if (!spellInfo || !SpellMgr::IsSpellValid(spellInfo))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!spellInfo->HasAura(SPELL_AURA_MOUNTED))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (target->HasSpell(i))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
target->learnSpell(i, false);
|
||||||
|
if (target->HasSpell(i))
|
||||||
|
++learned;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler->PSendSysMessage("Learned {} mount spell(s) for {}.", learned, handler->GetNameLink(target));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static void HandleLearnSkillRecipesHelper(Player* player, uint32 skillId)
|
static void HandleLearnSkillRecipesHelper(Player* player, uint32 skillId)
|
||||||
{
|
{
|
||||||
uint32 classmask = player->getClassMask();
|
uint32 classmask = player->getClassMask();
|
||||||
|
|||||||
@@ -64,10 +64,227 @@ struct npc_pet_mage_mirror_image : CasterAI
|
|||||||
uint32 dist = urand(1, 5);
|
uint32 dist = urand(1, 5);
|
||||||
bool _delayAttack;
|
bool _delayAttack;
|
||||||
|
|
||||||
|
// Fractured / Paragon: when the owner is a Paragon character with the
|
||||||
|
// wildcard config enabled, replace the stock Frostbolt + Fireblast
|
||||||
|
// allowlist (loaded by CombatAI from creature_template_spell for
|
||||||
|
// creature 31216) with a curated list of damaging spells from the
|
||||||
|
// owner's spellbook. UpdateAI's override picks a random spell from
|
||||||
|
// the list per cast so the rotation isn't deterministic.
|
||||||
|
//
|
||||||
|
// The image still casts as itself (not via the owner), so spell
|
||||||
|
// coefficients apply to the image's stats -- spells naturally do less
|
||||||
|
// damage than they would in the owner's hands. We accept that as the
|
||||||
|
// cost of "free cross-class spell variety" rather than try to rebalance
|
||||||
|
// every player spell here.
|
||||||
|
static bool IsDamagingForMirrorImage(SpellInfo const* si)
|
||||||
|
{
|
||||||
|
// Direct damage effect.
|
||||||
|
if (si->HasEffect(SPELL_EFFECT_SCHOOL_DAMAGE))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Spells like Arcane Missiles (TRIGGER_MISSILE) and most channeled
|
||||||
|
// multi-tick nukes route their damage through a child spell, so the
|
||||||
|
// parent has no SCHOOL_DAMAGE effect of its own. Accept that here.
|
||||||
|
if (si->HasEffect(SPELL_EFFECT_TRIGGER_MISSILE))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// DoTs and channels-as-aura (Mind Flay, Curse of Doom, Immolate,
|
||||||
|
// Corruption, Vampiric Touch, Drain Life leech, etc.). Also accept
|
||||||
|
// PERIODIC_TRIGGER_SPELL auras -- that's how Arcane Missiles fires
|
||||||
|
// each individual missile (parent has Aura=23 -> child damaging
|
||||||
|
// spell). Same pattern is used by Hunter Volley, Curse of Doom (in
|
||||||
|
// some ranks), and similar tick-by-trigger spells.
|
||||||
|
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
|
||||||
|
{
|
||||||
|
uint32 aura = si->Effects[i].ApplyAuraName;
|
||||||
|
if (aura == SPELL_AURA_PERIODIC_DAMAGE
|
||||||
|
|| aura == SPELL_AURA_PERIODIC_DAMAGE_PERCENT
|
||||||
|
|| aura == SPELL_AURA_PERIODIC_LEECH
|
||||||
|
|| aura == SPELL_AURA_PERIODIC_TRIGGER_SPELL)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RebuildSpellsFromOwnerSpellbookForParagon(Player* owner)
|
||||||
|
{
|
||||||
|
SpellVct curated;
|
||||||
|
curated.reserve(8);
|
||||||
|
|
||||||
|
uint32 scanned = 0, kept = 0, rejInactive = 0, rejPassive = 0, rejWeaponStrike = 0,
|
||||||
|
rejNoDmg = 0, rejAoe = 0, rejGate = 0, rejLongCD = 0, rejLowRank = 0;
|
||||||
|
|
||||||
|
// For diagnosis: collect IDs of spells we'd expect to keep (Fireball,
|
||||||
|
// Frostbolt, Lightning Bolt, Mind Blast, Shadow Bolt, etc.) but that
|
||||||
|
// we instead reject. The sample is small so per-spell logging is OK.
|
||||||
|
auto trackProbe = [&](uint32 spellId, char const* phase)
|
||||||
|
{
|
||||||
|
// Only log "interesting" spell IDs to avoid 177-line spam per image.
|
||||||
|
// These are first-rank IDs of common cross-class single-target nukes.
|
||||||
|
static constexpr uint32 probes[] = {
|
||||||
|
133, 116, 30451, // Mage: Fireball, Frostbolt, Arcane Blast
|
||||||
|
5143, // Mage: Arcane Missiles (channel via PERIODIC_TRIGGER_SPELL)
|
||||||
|
403, 529, 8042, // Shaman: Lightning Bolt, Chain Lightning, Earth Shock
|
||||||
|
585, 14914, // Priest: Smite, Holy Fire
|
||||||
|
8092, 15407, // Priest: Mind Blast, Mind Flay
|
||||||
|
686, 348, // Warlock: Shadow Bolt, Immolate (DoT w/ cast time)
|
||||||
|
5176, 2912, // Druid: Wrath, Starfire
|
||||||
|
635, // Paladin: Holy Light
|
||||||
|
};
|
||||||
|
for (uint32 probe : probes)
|
||||||
|
{
|
||||||
|
if (spellId == probe)
|
||||||
|
{
|
||||||
|
LOG_DEBUG("server.scripts",
|
||||||
|
"[paragon-diag] MirrorImage probe spell={} phase={}",
|
||||||
|
spellId, phase);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Also walk rank chain: if the spellbook has rank N of probe,
|
||||||
|
// probe matches via GetFirstRankSpell.
|
||||||
|
if (SpellInfo const* si = sSpellMgr->GetSpellInfo(spellId))
|
||||||
|
if (SpellInfo const* first = si->GetFirstRankSpell())
|
||||||
|
if (first->Id == probe)
|
||||||
|
{
|
||||||
|
LOG_DEBUG("server.scripts",
|
||||||
|
"[paragon-diag] MirrorImage probe spell={} (rank of {}) phase={}",
|
||||||
|
spellId, probe, phase);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto const& kv : owner->GetSpellMap())
|
||||||
|
{
|
||||||
|
++scanned;
|
||||||
|
uint32 spellId = kv.first;
|
||||||
|
PlayerSpell const* ps = kv.second;
|
||||||
|
if (!ps || ps->State == PLAYERSPELL_REMOVED || !ps->Active)
|
||||||
|
{
|
||||||
|
++rejInactive;
|
||||||
|
trackProbe(spellId, "inactive");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
|
||||||
|
if (!spellInfo)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Spec (per user): damaging single-target spells, instant or
|
||||||
|
// cast-time or channeled all OK, no melee/ranged "strike" style
|
||||||
|
// weapon-attack abilities, and no long-cooldown spells (>10s) so
|
||||||
|
// the image cycles through a varied rotation rather than blowing
|
||||||
|
// a 2-min cooldown once.
|
||||||
|
if (spellInfo->IsPassive()) { ++rejPassive; trackProbe(spellId, "passive"); continue; }
|
||||||
|
if (!IsDamagingForMirrorImage(spellInfo)) { ++rejNoDmg; trackProbe(spellId, "noDmg"); continue; }
|
||||||
|
if (spellInfo->IsAffectingArea()) { ++rejAoe; trackProbe(spellId, "aoe"); continue; }
|
||||||
|
if (spellInfo->DmgClass == SPELL_DAMAGE_CLASS_MELEE
|
||||||
|
|| spellInfo->DmgClass == SPELL_DAMAGE_CLASS_RANGED) { ++rejWeaponStrike; trackProbe(spellId, "weaponStrike"); continue; }
|
||||||
|
// Reject anything with a base cooldown longer than 10s (either
|
||||||
|
// RecoveryTime or CategoryRecoveryTime). A 0/very-short CD is
|
||||||
|
// fine. The mage Mirror Image only lives for 30s, so anything
|
||||||
|
// gated by a long CD would only ever fire once anyway.
|
||||||
|
uint32 cd = std::max(spellInfo->RecoveryTime, spellInfo->CategoryRecoveryTime);
|
||||||
|
if (cd > 10000) { ++rejLongCD; trackProbe(spellId, "longCD"); continue; }
|
||||||
|
|
||||||
|
// Skip spells the image would never realistically be able to
|
||||||
|
// cast successfully or whose side-effects don't make sense on a
|
||||||
|
// pet (totems, summons, item / reagent / focus requirements,
|
||||||
|
// ranged-weapon / shapeshift / stealth gates, profession spells,
|
||||||
|
// teleports, etc.).
|
||||||
|
char const* gateReason = nullptr;
|
||||||
|
if (spellInfo->RequiresSpellFocus) gateReason = "focus";
|
||||||
|
else if (spellInfo->Reagent[0] > 0) gateReason = "reagent";
|
||||||
|
else if (spellInfo->Stances || spellInfo->StancesNot) gateReason = "stance";
|
||||||
|
else if (spellInfo->EquippedItemClass >= 0) gateReason = "equipped";
|
||||||
|
else if (spellInfo->IsCooldownStartedOnEvent()) gateReason = "cdEvent";
|
||||||
|
else if (spellInfo->HasAttribute(SPELL_ATTR0_PASSIVE)) gateReason = "attrPassive";
|
||||||
|
else if (spellInfo->HasAttribute(SPELL_ATTR0_DO_NOT_DISPLAY)) gateReason = "attrHidden";
|
||||||
|
// SPELL_ATTR0_NOT_SHAPESHIFTED is intentionally NOT a gate -- it
|
||||||
|
// means "cannot be cast while caster IS shapeshifted", not "this
|
||||||
|
// spell requires a shapeshift". The attribute is set on every
|
||||||
|
// standard caster nuke (Fireball, Frostbolt, Lightning Bolt,
|
||||||
|
// Shadow Bolt, etc.) and Mirror Images are never shapeshifted,
|
||||||
|
// so the runtime check trivially passes for them. Filtering on
|
||||||
|
// it here was the bug that left the curated list empty.
|
||||||
|
else if (spellInfo->HasAttribute(SPELL_ATTR0_ONLY_STEALTHED)) gateReason = "attrStealth";
|
||||||
|
// SPELL_ATTR1_NO_AUTOCAST_AI is intentionally NOT a gate -- it is set
|
||||||
|
// on most player nukes (Fireball / Lightning Bolt / Shadow Bolt) to
|
||||||
|
// stop class pets from auto-casting them. Mirror Images are
|
||||||
|
// server-curated player-spell mimics, so we WANT to auto-cast
|
||||||
|
// those exact spells.
|
||||||
|
else if (spellInfo->HasAttribute(SPELL_ATTR2_FAIL_ON_ALL_TARGETS_IMMUNE)) gateReason = "attrFailImmune";
|
||||||
|
else if (spellInfo->HasEffect(SPELL_EFFECT_SUMMON)) gateReason = "fxSummon";
|
||||||
|
else if (spellInfo->HasEffect(SPELL_EFFECT_SUMMON_PET)) gateReason = "fxSummonPet";
|
||||||
|
else if (spellInfo->HasEffect(SPELL_EFFECT_TELEPORT_UNITS)) gateReason = "fxTeleport";
|
||||||
|
else if (spellInfo->HasEffect(SPELL_EFFECT_TRANS_DOOR)) gateReason = "fxTransDoor";
|
||||||
|
else if (spellInfo->HasEffect(SPELL_EFFECT_OPEN_LOCK)) gateReason = "fxOpenLock";
|
||||||
|
else if (spellInfo->HasEffect(SPELL_EFFECT_INSTAKILL)) gateReason = "fxInstakill";
|
||||||
|
else if (spellInfo->HasEffect(SPELL_EFFECT_LEARN_SPELL)) gateReason = "fxLearn";
|
||||||
|
if (gateReason) { ++rejGate; trackProbe(spellId, gateReason); continue; }
|
||||||
|
|
||||||
|
// Ignore spell ranks below the highest the player owns -- the
|
||||||
|
// spellbook contains all learned ranks; we want only the latest.
|
||||||
|
if (SpellInfo const* nextRank = spellInfo->GetNextRankSpell())
|
||||||
|
if (owner->HasSpell(nextRank->Id))
|
||||||
|
{ ++rejLowRank; trackProbe(spellId, "lowRank"); continue; }
|
||||||
|
|
||||||
|
++kept;
|
||||||
|
curated.push_back(spellId);
|
||||||
|
LOG_DEBUG("server.scripts",
|
||||||
|
"[paragon-diag] MirrorImage kept spell={} ({})",
|
||||||
|
spellId,
|
||||||
|
spellInfo->SpellName[0] ? spellInfo->SpellName[0] : "?");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("server.scripts",
|
||||||
|
"[paragon-diag] MirrorImage rebuild owner={} scanned={} kept={} "
|
||||||
|
"rejInactive={} rejPassive={} rejNoDmg={} rejAoe={} rejWeaponStrike={} rejLongCD={} rejGate={} rejLowRank={}",
|
||||||
|
owner->GetName(), scanned, kept,
|
||||||
|
rejInactive, rejPassive, rejNoDmg, rejAoe, rejWeaponStrike, rejLongCD, rejGate, rejLowRank);
|
||||||
|
|
||||||
|
if (curated.empty())
|
||||||
|
{
|
||||||
|
LOG_DEBUG("server.scripts",
|
||||||
|
"[paragon-diag] MirrorImage rebuild for {} produced empty list, keeping stock 59637/59638",
|
||||||
|
owner->GetName());
|
||||||
|
return; // keep stock 59637 / 59638 fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the first few spell IDs we picked so we can verify the list.
|
||||||
|
std::string sample;
|
||||||
|
for (size_t i = 0; i < curated.size() && i < 8; ++i)
|
||||||
|
{
|
||||||
|
if (!sample.empty())
|
||||||
|
sample += ',';
|
||||||
|
sample += std::to_string(curated[i]);
|
||||||
|
}
|
||||||
|
LOG_DEBUG("server.scripts",
|
||||||
|
"[paragon-diag] MirrorImage rebuild swapping spells for {} (sample: {})",
|
||||||
|
owner->GetName(), sample);
|
||||||
|
|
||||||
|
spells.swap(curated);
|
||||||
|
}
|
||||||
|
|
||||||
void InitializeAI() override
|
void InitializeAI() override
|
||||||
{
|
{
|
||||||
CasterAI::InitializeAI();
|
CasterAI::InitializeAI();
|
||||||
|
|
||||||
|
// Fractured / Paragon: do the spellbook rebuild EARLY -- before
|
||||||
|
// owner->CastSpell(CLONE_ME) and before any threat-list inheritance,
|
||||||
|
// because any of those can synchronously fire JustEngagedWith on the
|
||||||
|
// image and cause CasterAI::JustEngagedWith to schedule events from
|
||||||
|
// the stock [59638 Frostbolt, 59637 Fireblast] m_spells[] entries
|
||||||
|
// before our swap takes effect. The override of JustEngagedWith
|
||||||
|
// below also reasserts the swap + flushes events, so even if a later
|
||||||
|
// combat-entry path fires JustEngagedWith again it picks up the
|
||||||
|
// curated list.
|
||||||
|
if (Unit* owner = me->GetOwner())
|
||||||
|
if (Player* playerOwner = owner->ToPlayer())
|
||||||
|
if (IsParagonWildcardCaller(playerOwner))
|
||||||
|
RebuildSpellsFromOwnerSpellbookForParagon(playerOwner);
|
||||||
|
|
||||||
_delayAttack = true;
|
_delayAttack = true;
|
||||||
me->m_Events.AddEventAtOffset([this]()
|
me->m_Events.AddEventAtOffset([this]()
|
||||||
{
|
{
|
||||||
@@ -76,11 +293,21 @@ struct npc_pet_mage_mirror_image : CasterAI
|
|||||||
|
|
||||||
Unit* owner = me->GetOwner();
|
Unit* owner = me->GetOwner();
|
||||||
if (!owner)
|
if (!owner)
|
||||||
|
{
|
||||||
|
LOG_DEBUG("server.scripts",
|
||||||
|
"[paragon-diag] MirrorImage InitializeAI: no owner, spells.size={} (stock)",
|
||||||
|
spells.size());
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Clone Me!
|
// Clone Me!
|
||||||
owner->CastSpell(me, SPELL_MAGE_CLONE_ME, true);
|
owner->CastSpell(me, SPELL_MAGE_CLONE_ME, true);
|
||||||
|
|
||||||
|
LOG_DEBUG("server.scripts",
|
||||||
|
"[paragon-diag] MirrorImage InitializeAI: post-rebuild spells.size={} first={}",
|
||||||
|
spells.size(),
|
||||||
|
spells.empty() ? 0u : spells.front());
|
||||||
|
|
||||||
// xinef: Glyph of Mirror Image (4th copy)
|
// xinef: Glyph of Mirror Image (4th copy)
|
||||||
float angle = 0.0f;
|
float angle = 0.0f;
|
||||||
switch (me->GetUInt32Value(UNIT_CREATED_BY_SPELL))
|
switch (me->GetUInt32Value(UNIT_CREATED_BY_SPELL))
|
||||||
@@ -139,6 +366,37 @@ struct npc_pet_mage_mirror_image : CasterAI
|
|||||||
me->m_Events.AddEventAtOffset(new DeathEvent(*me), 29500ms);
|
me->m_Events.AddEventAtOffset(new DeathEvent(*me), 29500ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void JustEngagedWith(Unit* who) override
|
||||||
|
{
|
||||||
|
// Fractured / Paragon: re-apply the spellbook rebuild here as well,
|
||||||
|
// because the engagement can fire synchronously from inside
|
||||||
|
// InitializeAI (via owner->CastSpell(CLONE_ME) or summon-side threat
|
||||||
|
// propagation) BEFORE InitializeAI's own rebuild call has run.
|
||||||
|
// Re-running it here is cheap and idempotent: the curated list is
|
||||||
|
// re-derived from the owner's current spellbook, and we wipe any
|
||||||
|
// previously-scheduled events so the stock 59637 / 59638 entries
|
||||||
|
// CasterAI may already have queued get evicted before scheduling.
|
||||||
|
if (Unit* owner = me->GetOwner())
|
||||||
|
if (Player* playerOwner = owner->ToPlayer())
|
||||||
|
if (IsParagonWildcardCaller(playerOwner))
|
||||||
|
{
|
||||||
|
RebuildSpellsFromOwnerSpellbookForParagon(playerOwner);
|
||||||
|
events.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string sample;
|
||||||
|
for (size_t i = 0; i < spells.size() && i < 8; ++i)
|
||||||
|
{
|
||||||
|
if (!sample.empty()) sample += ',';
|
||||||
|
sample += std::to_string(spells[i]);
|
||||||
|
}
|
||||||
|
LOG_DEBUG("server.scripts",
|
||||||
|
"[paragon-diag] MirrorImage JustEngagedWith: spells.size={} sample=[{}] who={}",
|
||||||
|
spells.size(), sample, who ? who->GetName() : "<null>");
|
||||||
|
|
||||||
|
CasterAI::JustEngagedWith(who);
|
||||||
|
}
|
||||||
|
|
||||||
// Do not reload Creature templates on evade mode enter - prevent visual lost
|
// Do not reload Creature templates on evade mode enter - prevent visual lost
|
||||||
void EnterEvadeMode(EvadeReason /*why*/) override
|
void EnterEvadeMode(EvadeReason /*why*/) override
|
||||||
{
|
{
|
||||||
@@ -217,10 +475,61 @@ struct npc_pet_mage_mirror_image : CasterAI
|
|||||||
if (me->HasUnitState(UNIT_STATE_CASTING))
|
if (me->HasUnitState(UNIT_STATE_CASTING))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (uint32 spellId = events.ExecuteEvent())
|
if (uint32 queuedId = events.ExecuteEvent())
|
||||||
{
|
{
|
||||||
events.RescheduleEvent(spellId, spellId == 59637 ? 6500ms : 2500ms);
|
// Fractured / Paragon: when the curated spellbook list is in
|
||||||
me->CastSpell(me->GetVictim(), spellId, false);
|
// play, pick a random spell from it for THIS cast instead of
|
||||||
|
// using the EventMap-scheduled spellId directly. The events
|
||||||
|
// queue (populated by CasterAI::JustEngagedWith) is otherwise
|
||||||
|
// deterministic for our small list and the image ends up
|
||||||
|
// rotating in lockstep; randomizing here makes each image
|
||||||
|
// (and each cast) feel like a mage ad-libbing from the
|
||||||
|
// player's repertoire.
|
||||||
|
uint32 actualId = queuedId;
|
||||||
|
bool isParagon = false;
|
||||||
|
if (Unit* owner = me->GetOwner())
|
||||||
|
if (Player* playerOwner = owner->ToPlayer())
|
||||||
|
if (IsParagonWildcardCaller(playerOwner) && !spells.empty())
|
||||||
|
{
|
||||||
|
actualId = spells[urand(0, uint32(spells.size()) - 1)];
|
||||||
|
isParagon = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reschedule the queue based on the spell we actually cast,
|
||||||
|
// not the one originally queued. For channeled spells this
|
||||||
|
// matters: Arcane Missiles is a 5s channel, so if we keep
|
||||||
|
// rescheduling every 2.5s the image is always either mid-
|
||||||
|
// channel or immediately re-rolling for another channel,
|
||||||
|
// and over four images you see effectively continuous
|
||||||
|
// Arcane Missiles. Wait for cast/channel to finish + a
|
||||||
|
// small breather before picking again.
|
||||||
|
Milliseconds nextDelay = (queuedId == 59637 ? 6500ms : 2500ms);
|
||||||
|
if (isParagon)
|
||||||
|
{
|
||||||
|
if (SpellInfo const* picked = sSpellMgr->GetSpellInfo(actualId))
|
||||||
|
{
|
||||||
|
uint32 castMs = picked->CalcCastTime();
|
||||||
|
uint32 chanMs = 0;
|
||||||
|
if (picked->IsChanneled())
|
||||||
|
{
|
||||||
|
int32 dur = picked->GetDuration();
|
||||||
|
if (dur > 0)
|
||||||
|
chanMs = uint32(dur);
|
||||||
|
}
|
||||||
|
uint32 minMs = std::max(castMs, chanMs) + 750; // breather
|
||||||
|
if (Milliseconds(minMs) > nextDelay)
|
||||||
|
nextDelay = Milliseconds(minMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events.RescheduleEvent(queuedId, nextDelay);
|
||||||
|
|
||||||
|
SpellCastResult castRes = me->CastSpell(me->GetVictim(), actualId, false);
|
||||||
|
LOG_DEBUG("server.scripts",
|
||||||
|
"[paragon-diag] MirrorImage cast spell={} victim={} result={} nextDelay={}ms",
|
||||||
|
actualId,
|
||||||
|
me->GetVictim() ? me->GetVictim()->GetName() : "<null>",
|
||||||
|
uint32(castRes),
|
||||||
|
uint32(nextDelay.count()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ enum DeathKnightSpells
|
|||||||
SPELL_DK_ITEM_T8_MELEE_4P_BONUS = 64736,
|
SPELL_DK_ITEM_T8_MELEE_4P_BONUS = 64736,
|
||||||
SPELL_DK_MASTER_OF_GHOULS = 52143,
|
SPELL_DK_MASTER_OF_GHOULS = 52143,
|
||||||
SPELL_DK_BLOOD_PLAGUE = 55078,
|
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_RAISE_DEAD_USE_REAGENT = 48289,
|
||||||
SPELL_DK_RUNIC_POWER_ENERGIZE = 49088,
|
SPELL_DK_RUNIC_POWER_ENERGIZE = 49088,
|
||||||
SPELL_DK_SCENT_OF_BLOOD = 50422,
|
SPELL_DK_SCENT_OF_BLOOD = 50422,
|
||||||
@@ -107,6 +110,10 @@ enum DeathKnightSpells
|
|||||||
SPELL_DK_RUNE_STRIKE_OFF_HAND_R1 = 66217,
|
SPELL_DK_RUNE_STRIKE_OFF_HAND_R1 = 66217,
|
||||||
SPELL_DK_BLOOD_STRIKE_OFF_HAND_R1 = 66215,
|
SPELL_DK_BLOOD_STRIKE_OFF_HAND_R1 = 66215,
|
||||||
SPELL_DK_KILLING_MACHINE = 51124,
|
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
|
enum DeathKnightSpellIcons
|
||||||
@@ -126,6 +133,21 @@ enum Misc
|
|||||||
NPC_RISEN_ALLY = 30230
|
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
|
// 50526 - Wandering Plague
|
||||||
class spell_dk_wandering_plague : public SpellScript
|
class spell_dk_wandering_plague : public SpellScript
|
||||||
{
|
{
|
||||||
@@ -664,6 +686,23 @@ class spell_dk_dancing_rune_weapon : public AuraScript
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
SpellInfo const* spellInfo = eventInfo.GetSpellInfo();
|
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)
|
if (!spellInfo)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -1780,8 +1819,11 @@ class spell_dk_improved_blood_presence : public AuraScript
|
|||||||
return ValidateSpellInfo(
|
return ValidateSpellInfo(
|
||||||
{
|
{
|
||||||
SPELL_DK_BLOOD_PRESENCE,
|
SPELL_DK_BLOOD_PRESENCE,
|
||||||
|
SPELL_PARAGON_ADV_BLOOD_PRESENCE,
|
||||||
SPELL_DK_FROST_PRESENCE,
|
SPELL_DK_FROST_PRESENCE,
|
||||||
|
SPELL_PARAGON_ADV_FROST_PRESENCE,
|
||||||
SPELL_DK_UNHOLY_PRESENCE,
|
SPELL_DK_UNHOLY_PRESENCE,
|
||||||
|
SPELL_PARAGON_ADV_UNHOLY_PRESENCE,
|
||||||
SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED
|
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*/)
|
void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/)
|
||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
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);
|
target->CastCustomSpell(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED, SPELLVALUE_BASE_POINT1, aurEff->GetAmount(), target, true, nullptr, aurEff);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)
|
void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)
|
||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target->HasAura(SPELL_DK_BLOOD_PRESENCE))
|
if (!Fractured_UnitHasBloodPresenceAura(target))
|
||||||
target->RemoveAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED);
|
target->RemoveAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1817,8 +1859,11 @@ class spell_dk_improved_frost_presence : public AuraScript
|
|||||||
return ValidateSpellInfo(
|
return ValidateSpellInfo(
|
||||||
{
|
{
|
||||||
SPELL_DK_BLOOD_PRESENCE,
|
SPELL_DK_BLOOD_PRESENCE,
|
||||||
|
SPELL_PARAGON_ADV_BLOOD_PRESENCE,
|
||||||
SPELL_DK_FROST_PRESENCE,
|
SPELL_DK_FROST_PRESENCE,
|
||||||
|
SPELL_PARAGON_ADV_FROST_PRESENCE,
|
||||||
SPELL_DK_UNHOLY_PRESENCE,
|
SPELL_DK_UNHOLY_PRESENCE,
|
||||||
|
SPELL_PARAGON_ADV_UNHOLY_PRESENCE,
|
||||||
SPELL_DK_FROST_PRESENCE_TRIGGERED
|
SPELL_DK_FROST_PRESENCE_TRIGGERED
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1826,14 +1871,14 @@ class spell_dk_improved_frost_presence : public AuraScript
|
|||||||
void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/)
|
void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/)
|
||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
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);
|
target->CastCustomSpell(SPELL_DK_FROST_PRESENCE_TRIGGERED, SPELLVALUE_BASE_POINT0, aurEff->GetAmount(), target, true, nullptr, aurEff);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)
|
void HandleEffectRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)
|
||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
Unit* target = GetTarget();
|
||||||
if (!target->HasAura(SPELL_DK_FROST_PRESENCE))
|
if (!Fractured_UnitHasFrostPresenceAura(target))
|
||||||
target->RemoveAura(SPELL_DK_FROST_PRESENCE_TRIGGERED);
|
target->RemoveAura(SPELL_DK_FROST_PRESENCE_TRIGGERED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1854,8 +1899,11 @@ class spell_dk_improved_unholy_presence : public AuraScript
|
|||||||
return ValidateSpellInfo(
|
return ValidateSpellInfo(
|
||||||
{
|
{
|
||||||
SPELL_DK_BLOOD_PRESENCE,
|
SPELL_DK_BLOOD_PRESENCE,
|
||||||
|
SPELL_PARAGON_ADV_BLOOD_PRESENCE,
|
||||||
SPELL_DK_FROST_PRESENCE,
|
SPELL_DK_FROST_PRESENCE,
|
||||||
|
SPELL_PARAGON_ADV_FROST_PRESENCE,
|
||||||
SPELL_DK_UNHOLY_PRESENCE,
|
SPELL_DK_UNHOLY_PRESENCE,
|
||||||
|
SPELL_PARAGON_ADV_UNHOLY_PRESENCE,
|
||||||
SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED,
|
SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED,
|
||||||
SPELL_DK_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*/)
|
void HandleEffectApply(AuraEffect const* aurEff, AuraEffectHandleModes /*mode*/)
|
||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
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
|
// Not listed as any effect, only base points set in dbc
|
||||||
int32 basePoints = GetSpellInfo()->Effects[EFFECT_1].CalcValue();
|
int32 basePoints = GetSpellInfo()->Effects[EFFECT_1].CalcValue();
|
||||||
target->CastCustomSpell(target, SPELL_DK_IMPROVED_UNHOLY_PRESENCE_TRIGGERED, &basePoints, &basePoints, &basePoints, true, nullptr, aurEff);
|
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);
|
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);
|
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);
|
target->RemoveAura(SPELL_DK_UNHOLY_PRESENCE_TRIGGERED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1916,6 +1964,20 @@ class spell_dk_pestilence : public SpellScript
|
|||||||
if (!target)
|
if (!target)
|
||||||
return;
|
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
|
// Spread on others
|
||||||
if (target != hitUnit)
|
if (target != hitUnit)
|
||||||
{
|
{
|
||||||
@@ -1926,6 +1988,17 @@ class spell_dk_pestilence : public SpellScript
|
|||||||
// Frost Fever
|
// Frost Fever
|
||||||
if (target->GetAura(SPELL_DK_FROST_FEVER, caster->GetGUID()))
|
if (target->GetAura(SPELL_DK_FROST_FEVER, caster->GetGUID()))
|
||||||
caster->CastSpell(hitUnit, SPELL_DK_FROST_FEVER, true);
|
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
|
// Refresh on target
|
||||||
else if (caster->GetAura(SPELL_DK_GLYPH_OF_DISEASE))
|
else if (caster->GetAura(SPELL_DK_GLYPH_OF_DISEASE))
|
||||||
@@ -1946,6 +2019,16 @@ class spell_dk_pestilence : public SpellScript
|
|||||||
disease->RefreshDuration();
|
disease->RefreshDuration();
|
||||||
else if (Aura* disease = target->GetAuraOfRankedSpell(SPELL_DK_CRYPT_FEVER_R1, caster->GetGUID()))
|
else if (Aura* disease = target->GetAuraOfRankedSpell(SPELL_DK_CRYPT_FEVER_R1, caster->GetGUID()))
|
||||||
disease->RefreshDuration();
|
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_BLOOD_PRESENCE,
|
||||||
SPELL_DK_FROST_PRESENCE,
|
SPELL_DK_FROST_PRESENCE,
|
||||||
SPELL_DK_UNHOLY_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_BLOOD_PRESENCE_R1,
|
||||||
SPELL_DK_IMPROVED_FROST_PRESENCE_R1,
|
SPELL_DK_IMPROVED_FROST_PRESENCE_R1,
|
||||||
SPELL_DK_IMPROVED_UNHOLY_PRESENCE_R1,
|
SPELL_DK_IMPROVED_UNHOLY_PRESENCE_R1,
|
||||||
@@ -1983,7 +2069,7 @@ class spell_dk_presence : public AuraScript
|
|||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
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);
|
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))
|
else if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_BLOOD_PRESENCE_R1, EFFECT_0))
|
||||||
if (!target->HasAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED))
|
if (!target->HasAura(SPELL_DK_IMPROVED_BLOOD_PRESENCE_TRIGGERED))
|
||||||
@@ -1994,7 +2080,7 @@ class spell_dk_presence : public AuraScript
|
|||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
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);
|
target->CastSpell(target, SPELL_DK_FROST_PRESENCE_TRIGGERED, true);
|
||||||
else if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_FROST_PRESENCE_R1, EFFECT_0))
|
else if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_FROST_PRESENCE_R1, EFFECT_0))
|
||||||
if (!target->HasAura(SPELL_DK_FROST_PRESENCE_TRIGGERED))
|
if (!target->HasAura(SPELL_DK_FROST_PRESENCE_TRIGGERED))
|
||||||
@@ -2005,12 +2091,12 @@ class spell_dk_presence : public AuraScript
|
|||||||
{
|
{
|
||||||
Unit* target = GetTarget();
|
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);
|
target->CastSpell(target, SPELL_DK_UNHOLY_PRESENCE_TRIGGERED, true);
|
||||||
|
|
||||||
if (AuraEffect const* impAurEff = target->GetAuraEffectOfRankedSpell(SPELL_DK_IMPROVED_UNHOLY_PRESENCE_R1, EFFECT_0))
|
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
|
// Not listed as any effect, only base points set
|
||||||
int32 bp = impAurEff->GetSpellInfo()->Effects[EFFECT_1].CalcValue();
|
int32 bp = impAurEff->GetSpellInfo()->Effects[EFFECT_1].CalcValue();
|
||||||
|
|||||||
@@ -490,12 +490,22 @@ class spell_mage_cold_snap : public SpellScript
|
|||||||
{
|
{
|
||||||
Player* caster = GetCaster()->ToPlayer();
|
Player* caster = GetCaster()->ToPlayer();
|
||||||
// immediately finishes the cooldown on Frost spells
|
// immediately finishes the cooldown on Frost spells
|
||||||
|
//
|
||||||
|
// Fractured / Paragon: ParagonFamilyMatches() drops the
|
||||||
|
// SpellFamilyName == SPELLFAMILY_MAGE gate when the caster is a
|
||||||
|
// CLASS_PARAGON player AND Paragon.WildcardFamilyMatching is on,
|
||||||
|
// so any Frost-school spell in the Paragon's spellbook with a real
|
||||||
|
// recovery time (Howling Blast, Frost Shock, Frost Trap, etc.)
|
||||||
|
// also gets its cooldown wiped. Stock Mage callers fall through to
|
||||||
|
// strict family-name equality and observe identical behavior.
|
||||||
PlayerSpellMap const& spellMap = caster->GetSpellMap();
|
PlayerSpellMap const& spellMap = caster->GetSpellMap();
|
||||||
for (PlayerSpellMap::const_iterator itr = spellMap.begin(); itr != spellMap.end(); ++itr)
|
for (PlayerSpellMap::const_iterator itr = spellMap.begin(); itr != spellMap.end(); ++itr)
|
||||||
{
|
{
|
||||||
SpellInfo const* spellInfo = sSpellMgr->AssertSpellInfo(itr->first);
|
SpellInfo const* spellInfo = sSpellMgr->AssertSpellInfo(itr->first);
|
||||||
if (spellInfo->SpellFamilyName == SPELLFAMILY_MAGE && (spellInfo->GetSchoolMask() & SPELL_SCHOOL_MASK_FROST) && spellInfo->Id != SPELL_MAGE_COLD_SNAP && spellInfo->GetRecoveryTime() > 0)
|
if (ParagonFamilyMatches(caster, SPELLFAMILY_MAGE, spellInfo->SpellFamilyName)
|
||||||
|
&& (spellInfo->GetSchoolMask() & SPELL_SCHOOL_MASK_FROST)
|
||||||
|
&& spellInfo->Id != SPELL_MAGE_COLD_SNAP
|
||||||
|
&& spellInfo->GetRecoveryTime() > 0)
|
||||||
{
|
{
|
||||||
SpellCooldowns::iterator citr = caster->GetSpellCooldownMap().find(spellInfo->Id);
|
SpellCooldowns::iterator citr = caster->GetSpellCooldownMap().find(spellInfo->Id);
|
||||||
if (citr != caster->GetSpellCooldownMap().end() && citr->second.needSendToClient)
|
if (citr != caster->GetSpellCooldownMap().end() && citr->second.needSendToClient)
|
||||||
@@ -946,6 +956,107 @@ class spell_mage_summon_water_elemental : public SpellScript
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 44543, 44545 - Fingers of Frost (talent ranks - the proc-trigger aura, NOT the
|
||||||
|
// 74396 buff aura that is APPLIED when this talent fires).
|
||||||
|
//
|
||||||
|
// Stock spell_proc gates this talent by SpellFamilyName=MAGE plus a
|
||||||
|
// SpellFamilyMask covering the Mage Frost spells that count as "chill-effect
|
||||||
|
// dealers" (Frostbolt / Frost Nova / Cone of Cold / Blizzard / Frostfire Bolt /
|
||||||
|
// Deep Freeze etc.). For Paragon characters with `Paragon.WildcardFamilyMatching`
|
||||||
|
// enabled, we relax the spell_proc row to wildcard family/mask + SchoolMask=
|
||||||
|
// FROST + SpellTypeMask=DAMAGE so that any Frost-school damage spell (DK Howling
|
||||||
|
// Blast / Icy Touch, Hunter Frost Trap / Wing Clip-as-frost, Shaman Frost Shock,
|
||||||
|
// Druid Hibernate damage payload, etc.) reaches this CheckProc; this script
|
||||||
|
// then re-enforces the stock Mage allowlist for non-Paragon owners and lets
|
||||||
|
// Paragons through unconditionally (the FROST + DAMAGE gate already happens at
|
||||||
|
// the spell_proc layer, so any spell reaching us here is safe to accept).
|
||||||
|
class spell_mage_fingers_of_frost_talent : public AuraScript
|
||||||
|
{
|
||||||
|
PrepareAuraScript(spell_mage_fingers_of_frost_talent);
|
||||||
|
|
||||||
|
bool CheckProc(ProcEventInfo& eventInfo)
|
||||||
|
{
|
||||||
|
SpellInfo const* procSpell = eventInfo.GetSpellInfo();
|
||||||
|
if (!procSpell)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Stock Mage allowlist: re-derive from this talent's own effect-0
|
||||||
|
// SpellClassMask so behavior matches the original auto-generated
|
||||||
|
// proc filter exactly (no risk of mask drift across DBC versions).
|
||||||
|
if (procSpell->SpellFamilyName == SPELLFAMILY_MAGE
|
||||||
|
&& (GetSpellInfo()->Effects[EFFECT_0].SpellClassMask & procSpell->SpellFamilyFlags))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return IsParagonWildcardCaller(GetUnitOwner());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Register() override
|
||||||
|
{
|
||||||
|
DoCheckProc += AuraCheckProcFn(spell_mage_fingers_of_frost_talent::CheckProc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 11071, 12496, 12497 - Frostbite (talent ranks - the proc-trigger aura that
|
||||||
|
// chains into 12494 Frostbite freeze).
|
||||||
|
//
|
||||||
|
// Stock spell_proc (auto-generated from DBC) gates this talent by Mage family +
|
||||||
|
// the talent's effect SpellClassMask (Mage Frost slow-applying spells). For
|
||||||
|
// Paragon characters we relax the row to SchoolMask=FROST wildcard so that
|
||||||
|
// chill-applying Frost spells from any class can reach this CheckProc; the
|
||||||
|
// Paragon path additionally requires the proc spell to actually apply a slow
|
||||||
|
// (SPELL_AURA_MOD_DECREASE_SPEED) so that pure damage Frost spells without a
|
||||||
|
// chill component (e.g. raw Ice Lance on a non-frozen target) do NOT freeze.
|
||||||
|
// Stock Mage owners get the original behavior re-enforced here.
|
||||||
|
class spell_mage_frostbite : public AuraScript
|
||||||
|
{
|
||||||
|
PrepareAuraScript(spell_mage_frostbite);
|
||||||
|
|
||||||
|
bool CheckProc(ProcEventInfo& eventInfo)
|
||||||
|
{
|
||||||
|
SpellInfo const* procSpell = eventInfo.GetSpellInfo();
|
||||||
|
if (!procSpell)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Stock Mage path: re-derive from this talent's own effect-0
|
||||||
|
// SpellClassMask so behavior matches the original auto-generated
|
||||||
|
// proc filter exactly (no risk of mask drift across DBC versions).
|
||||||
|
if (procSpell->SpellFamilyName == SPELLFAMILY_MAGE
|
||||||
|
&& (GetSpellInfo()->Effects[EFFECT_0].SpellClassMask & procSpell->SpellFamilyFlags))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!IsParagonWildcardCaller(GetUnitOwner()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Paragon path: any Frost-school spell that applies a chill effect
|
||||||
|
// (decrease-speed aura). The spell_proc row already gates by
|
||||||
|
// SchoolMask=FROST so we only need to verify chill semantics here.
|
||||||
|
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
|
||||||
|
{
|
||||||
|
if (procSpell->Effects[i].ApplyAuraName == SPELL_AURA_MOD_DECREASE_SPEED)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also accept the Improved-Blizzard-style cross-class case where the
|
||||||
|
// chill is applied by a separate triggered aura: if the proc spell's
|
||||||
|
// damage hit landed and the target already has a chill from us, treat
|
||||||
|
// it as eligible. Cheap and matches player expectations for Paragon.
|
||||||
|
if (Unit* procTarget = eventInfo.GetProcTarget())
|
||||||
|
{
|
||||||
|
Unit::AuraEffectList const& slows = procTarget->GetAuraEffectsByType(SPELL_AURA_MOD_DECREASE_SPEED);
|
||||||
|
for (AuraEffect const* slowEff : slows)
|
||||||
|
if (slowEff->GetCasterGUID() == GetUnitOwner()->GetGUID())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Register() override
|
||||||
|
{
|
||||||
|
DoCheckProc += AuraCheckProcFn(spell_mage_frostbite::CheckProc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 74396 - Fingers of Frost
|
// 74396 - Fingers of Frost
|
||||||
class spell_mage_fingers_of_frost : public AuraScript
|
class spell_mage_fingers_of_frost : public AuraScript
|
||||||
{
|
{
|
||||||
@@ -1631,5 +1742,7 @@ void AddSC_mage_spell_scripts()
|
|||||||
RegisterSpellScript(spell_mage_polymorph_cast_visual);
|
RegisterSpellScript(spell_mage_polymorph_cast_visual);
|
||||||
RegisterSpellScript(spell_mage_summon_water_elemental);
|
RegisterSpellScript(spell_mage_summon_water_elemental);
|
||||||
RegisterSpellScript(spell_mage_fingers_of_frost);
|
RegisterSpellScript(spell_mage_fingers_of_frost);
|
||||||
|
RegisterSpellScript(spell_mage_fingers_of_frost_talent);
|
||||||
|
RegisterSpellScript(spell_mage_frostbite);
|
||||||
RegisterSpellScript(spell_mage_magic_absorption);
|
RegisterSpellScript(spell_mage_magic_absorption);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1005,12 +1005,38 @@ class spell_pri_vampiric_embrace : public AuraScript
|
|||||||
|
|
||||||
bool CheckProc(ProcEventInfo& eventInfo)
|
bool CheckProc(ProcEventInfo& eventInfo)
|
||||||
{
|
{
|
||||||
// Not proc from Mind Sear
|
|
||||||
SpellInfo const* procSpell = eventInfo.GetSpellInfo();
|
SpellInfo const* procSpell = eventInfo.GetSpellInfo();
|
||||||
if (!procSpell)
|
if (!procSpell)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return !(procSpell->SpellFamilyFlags[1] & 0x80000);
|
// Stock: filter Mind Sear (the damage-tick spell carries this
|
||||||
|
// SpellFamilyFlags[1] bit; the channel itself is filtered by the
|
||||||
|
// standard data-row mask). Kept as a bit-test so the stock priest
|
||||||
|
// path is byte-identical to before this change.
|
||||||
|
if (procSpell->SpellFamilyFlags[1] & 0x80000)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Fractured / Paragon: any single-target Shadow-school damage spell
|
||||||
|
// procs Vampiric Embrace, not just Priest Shadow spells. The
|
||||||
|
// SchoolMask=Shadow gate is enforced by the spell_proc data row
|
||||||
|
// (SchoolMask=32). The data-row family/mask was wildcarded in
|
||||||
|
// mod-paragon's 2026_05_11_01.sql update so this CheckProc fires for
|
||||||
|
// cross-family Shadow spells; here we add the single-target
|
||||||
|
// requirement (Mind Sear was already filtered above; this also
|
||||||
|
// catches AoE Warlock Shadow spells like Seed of Corruption,
|
||||||
|
// Hellfire, etc. that a Paragon could otherwise cast).
|
||||||
|
if (IsParagonWildcardCaller(GetTarget()))
|
||||||
|
return !procSpell->IsAffectingArea();
|
||||||
|
|
||||||
|
// Stock priest path: re-enforce the original Priest Shadow damage
|
||||||
|
// gate that used to live entirely in the data row. Without this,
|
||||||
|
// wildcarding the data row would let item-cast Shadow effects
|
||||||
|
// (consumables, trinkets) accidentally proc VE on stock priests.
|
||||||
|
if (procSpell->SpellFamilyName != SPELLFAMILY_PRIEST)
|
||||||
|
return false;
|
||||||
|
return (procSpell->SpellFamilyFlags[0] & 0x0280A010)
|
||||||
|
|| (procSpell->SpellFamilyFlags[1] & 0x00002402)
|
||||||
|
|| (procSpell->SpellFamilyFlags[2] & 0x00000008);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleProc(AuraEffect const* aurEff, ProcEventInfo& eventInfo)
|
void HandleProc(AuraEffect const* aurEff, ProcEventInfo& eventInfo)
|
||||||
|
|||||||
@@ -1790,6 +1790,45 @@ 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 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();
|
||||||
|
if (!procSpell)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Stock allowlist (Shaman): Lightning Bolt, Chain Lightning,
|
||||||
|
// Lesser Healing Wave, Healing Wave, Hex. Encoded as the original
|
||||||
|
// SpellFamilyMask values from the pre-relaxation spell_proc row
|
||||||
|
// (Mask0 = 451, Mask1 = 32768).
|
||||||
|
bool stockMatch = procSpell->SpellFamilyName == SPELLFAMILY_SHAMAN
|
||||||
|
&& ((procSpell->SpellFamilyFlags[0] & 451u)
|
||||||
|
|| (procSpell->SpellFamilyFlags[1] & 32768u));
|
||||||
|
if (stockMatch)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!IsParagonWildcardCaller(GetUnitOwner()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Paragon path: also accept the curated Mage cast-time nukes.
|
||||||
|
if (procSpell->SpellFamilyName == SPELLFAMILY_MAGE)
|
||||||
|
{
|
||||||
|
SpellInfo const* first = procSpell->GetFirstRankSpell();
|
||||||
|
uint32 firstId = first ? first->Id : procSpell->Id;
|
||||||
|
if (firstId == 133 /*Fireball*/
|
||||||
|
|| firstId == 116 /*Frostbolt*/)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void HandleBonus(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)
|
void HandleBonus(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)
|
||||||
{
|
{
|
||||||
if (GetStackAmount() < int32(GetSpellInfo()->StackAmount))
|
if (GetStackAmount() < int32(GetSpellInfo()->StackAmount))
|
||||||
@@ -1805,6 +1844,7 @@ class spell_sha_maelstrom_weapon : public AuraScript
|
|||||||
|
|
||||||
void Register() override
|
void Register() override
|
||||||
{
|
{
|
||||||
|
DoCheckProc += AuraCheckProcFn(spell_sha_maelstrom_weapon::CheckProc);
|
||||||
OnEffectApply += AuraEffectApplyFn(spell_sha_maelstrom_weapon::HandleBonus, EFFECT_0, SPELL_AURA_ADD_PCT_MODIFIER, AURA_EFFECT_HANDLE_CHANGE_AMOUNT);
|
OnEffectApply += AuraEffectApplyFn(spell_sha_maelstrom_weapon::HandleBonus, EFFECT_0, SPELL_AURA_ADD_PCT_MODIFIER, AURA_EFFECT_HANDLE_CHANGE_AMOUNT);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -115,6 +115,17 @@ CI workflow **Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml
|
|||||||
|
|
||||||
**Manual upload:** `bash scripts/upload-release-to-gitea.sh /path/to/files v1.0.0` with the same env vars as CI.
|
**Manual upload:** `bash scripts/upload-release-to-gitea.sh /path/to/files v1.0.0` with the same env vars as CI.
|
||||||
|
|
||||||
|
**Legacy “bridge” after changing `baked-gitea-channel.js`:** Players still using the old Gitea URL only receive launcher updates from that host. Build **Windows + Linux** installers (e.g. download **Fractured launcher CI** artifacts, or run **`npm run pack:win`** / **`npm run pack:linux`**), put **`dist/`** contents in one folder if needed, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GITEA_BASE_URL=http://your-old-host:port # legacy base, no trailing slash
|
||||||
|
export GITEA_TOKEN=... GITEA_OWNER=Dawnsorrow GITEA_REPO=Fractured-Distro
|
||||||
|
bash tools/fractured-launcher-electron/scripts/gitea-replace-launcher-only.sh \
|
||||||
|
tools/fractured-launcher-electron/dist latest
|
||||||
|
```
|
||||||
|
|
||||||
|
That script deletes only **`Fractured-Launcher*`**, **`latest.yml`** (only if you supply a new **`latest.yml`** in **`dist/`**), **`latest-linux.yml`** (only if supplied), **`*.blockmap`**, and **`builder-debug.yml`** on the release, then uploads the new files — **Wow.exe**, MPQs, and **`patch-manifest.json`** are left alone. If you only built Linux locally, merge **Windows CI `dist/`** files into the same folder first so **`latest.yml`** is not removed without a replacement. Use release tag **`latest`** if that is what **`release_tag`** points at.
|
||||||
|
|
||||||
### Sync did not run / Gitea unchanged — checklist
|
### Sync did not run / Gitea unchanged — checklist
|
||||||
|
|
||||||
1. **Git tag ≠ GitHub Release** — Only **Releases** (published on the GitHub **Releases** page) trigger this workflow. If your teammate only **`git push --tags`**, create a **Release** from that tag and click **Publish** (or run **Actions → Sync release to Gitea → Run workflow** and enter the tag).
|
1. **Git tag ≠ GitHub Release** — Only **Releases** (published on the GitHub **Releases** page) trigger this workflow. If your teammate only **`git push --tags`**, create a **Release** from that tag and click **Publish** (or run **Actions → Sync release to Gitea → Run workflow** and enter the tag).
|
||||||
@@ -130,6 +141,7 @@ CI workflow **Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml
|
|||||||
11. **`EBUSY` / file locked on Windows** — The client (or antivirus) may keep **`.MPQ`** files open. The launcher **retries** for a short window and downloads the new file **before** replacing the old one; if sync still fails, **exit WoW** (and any tool previewing that folder) and run **Download updates** again.
|
11. **`EBUSY` / file locked on Windows** — The client (or antivirus) may keep **`.MPQ`** files open. The launcher **retries** for a short window and downloads the new file **before** replacing the old one; if sync still fails, **exit WoW** (and any tool previewing that folder) and run **Download updates** again.
|
||||||
12. **`.bak-*` clutter** — When **Download updates** finishes without error, the launcher removes matching **`*.bak-YYYYMMDD-HHmmss`** files from earlier runs (same pattern it uses when replacing files). Failed syncs do not delete backups.
|
12. **`.bak-*` clutter** — When **Download updates** finishes without error, the launcher removes matching **`*.bak-YYYYMMDD-HHmmss`** files from earlier runs (same pattern it uses when replacing files). Failed syncs do not delete backups.
|
||||||
13. **Gitea still shows an old launcher version** — The sync workflow overlays **`tools/fractured-launcher-electron` from the default branch**, so **`package.json`** there defines the built version. Ensure launcher changes are **merged to `main`** before publishing the GitHub release (or re-run **Actions → Sync release to Gitea** after merging). Previously, **Fractured-Launcher\*** files **attached on the GitHub release** were merged too, which could leave **two** installer versions on Gitea; those assets are now skipped in favor of CI-only builds.
|
13. **Gitea still shows an old launcher version** — The sync workflow overlays **`tools/fractured-launcher-electron` from the default branch**, so **`package.json`** there defines the built version. Ensure launcher changes are **merged to `main`** before publishing the GitHub release (or re-run **Actions → Sync release to Gitea** after merging). Previously, **Fractured-Launcher\*** files **attached on the GitHub release** were merged too, which could leave **two** installer versions on Gitea; those assets are now skipped in favor of CI-only builds.
|
||||||
|
14. **Migrating `baked-gitea-channel.js` to a new host** — Publish **`gitea-replace-launcher-only.sh`** (see **Manual upload** above) on the **old** Gitea **`latest`** release so auto-update still works until clients move to the new URL.
|
||||||
|
|
||||||
### Private Gitea token for players
|
### Private Gitea token for players
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
* Production Gitea mirror (non-secret). Edit here and ship — no inject script,
|
* Production Gitea mirror (non-secret). Edit here and ship — no inject script,
|
||||||
* no fractured-release-channel.json, no CI env needed for these fields.
|
* no fractured-release-channel.json, no CI env needed for these fields.
|
||||||
* Token stays in env: GITEA_TOKEN or launcher.json → gitea.token_env.
|
* Token stays in env: GITEA_TOKEN or launcher.json → gitea.token_env.
|
||||||
|
*
|
||||||
|
* Use origin only (no /releases path): API is {base_url}/api/v1/…
|
||||||
|
* Web: https://git.hisora.dev/Dawnsorrow/Fractured-Distro/releases
|
||||||
*/
|
*/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// http:// kept as-is; bare host gets https in gitea-release.js
|
base_url: 'https://git.hisora.dev',
|
||||||
base_url: 'http://brassnet.ddns.net:33983',
|
|
||||||
owner: 'Dawnsorrow',
|
owner: 'Dawnsorrow',
|
||||||
repo: 'Fractured-Distro',
|
repo: 'Fractured-Distro',
|
||||||
release_tag: 'latest',
|
release_tag: 'latest',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fractured-launcher-electron",
|
"name": "fractured-launcher-electron",
|
||||||
"version": "1.0.12",
|
"version": "1.0.13",
|
||||||
"description": "Fractured WoW launcher (Electron) — no console window, native folder picker, auto-update",
|
"description": "Fractured WoW launcher (Electron) — no console window, native folder picker, auto-update",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Replace only launcher installer + latest.yml attachments on a Gitea release.
|
||||||
|
# Does NOT delete Wow.exe, MPQs, or patch-manifest — use this to publish a
|
||||||
|
# "bridge" build (e.g. 1.0.13 with new baked Gitea URL) on a legacy host while
|
||||||
|
# keeping game assets already on that release.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# export GITEA_BASE_URL=http://legacy-host:port # or https://...
|
||||||
|
# export GITEA_TOKEN=gta_...
|
||||||
|
# export GITEA_OWNER=Dawnsorrow
|
||||||
|
# export GITEA_REPO=Fractured-Distro
|
||||||
|
# ./gitea-replace-launcher-only.sh /path/to/electron/dist latest
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
# shellcheck source=release-sync-filters.sh
|
||||||
|
. "$SCRIPT_DIR/release-sync-filters.sh"
|
||||||
|
|
||||||
|
DIST_DIR="${1:?first arg: electron-builder dist directory (contains .exe / .AppImage / latest*.yml)}"
|
||||||
|
TAG="${2:?second arg: release tag (e.g. latest)}"
|
||||||
|
|
||||||
|
: "${GITEA_BASE_URL:?Set GITEA_BASE_URL}"
|
||||||
|
: "${GITEA_TOKEN:?Set GITEA_TOKEN}"
|
||||||
|
: "${GITEA_OWNER:?Set GITEA_OWNER}"
|
||||||
|
: "${GITEA_REPO:?Set GITEA_REPO}"
|
||||||
|
|
||||||
|
BASE="${GITEA_BASE_URL%/}"
|
||||||
|
API="$BASE/api/v1"
|
||||||
|
AUTH_H=(-H "Authorization: token ${GITEA_TOKEN}" -H "Accept: application/json")
|
||||||
|
|
||||||
|
TAG_ENC=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$TAG")
|
||||||
|
REL_JSON=$(mktemp)
|
||||||
|
trap 'rm -f "$REL_JSON"' EXIT
|
||||||
|
|
||||||
|
code=$(curl -sS -o "$REL_JSON" -w "%{http_code}" "${AUTH_H[@]}" \
|
||||||
|
"$API/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/tags/${TAG_ENC}")
|
||||||
|
|
||||||
|
if [ "$code" != "200" ]; then
|
||||||
|
echo "Gitea GET release by tag failed HTTP $code (release must already exist):" >&2
|
||||||
|
cat "$REL_JSON" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rel_id=$(jq -r '.id' "$REL_JSON")
|
||||||
|
if [ -z "$rel_id" ] || [ "$rel_id" = "null" ]; then
|
||||||
|
echo "Could not resolve Gitea release id" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
should_delete_attachment() {
|
||||||
|
local l
|
||||||
|
l=$(printf '%s' "${1##*/}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
case "$l" in
|
||||||
|
fractured-launcher*) return 0 ;;
|
||||||
|
*.blockmap) return 0 ;;
|
||||||
|
builder-debug.yml|builder-debug.yaml) return 0 ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
should_delete_yml_attachment() {
|
||||||
|
local l
|
||||||
|
l=$(printf '%s' "${1##*/}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
case "$l" in
|
||||||
|
latest.yml) [ -f "$DIST_DIR/latest.yml" ] ;;
|
||||||
|
latest-linux.yml) [ -f "$DIST_DIR/latest-linux.yml" ] ;;
|
||||||
|
latest-mac.yml) [ -f "$DIST_DIR/latest-mac.yml" ] ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
while read -r line; do
|
||||||
|
[ -z "$line" ] && continue
|
||||||
|
aid=$(printf '%s' "$line" | cut -f1)
|
||||||
|
aname=$(printf '%s' "$line" | cut -f2-)
|
||||||
|
if should_delete_attachment "$aname" || should_delete_yml_attachment "$aname"; then
|
||||||
|
echo "Removing old attachment: $aname (id=$aid)"
|
||||||
|
curl -fsS -X DELETE "${AUTH_H[@]}" \
|
||||||
|
"$API/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${rel_id}/assets/${aid}" || true
|
||||||
|
fi
|
||||||
|
done < <(jq -r '(.attachments // .assets // [])[] | "\(.id)\t\(.name)"' "$REL_JSON")
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
upload_paths=()
|
||||||
|
for f in "$DIST_DIR"/Fractured-Launcher*.exe "$DIST_DIR"/Fractured-Launcher*.AppImage \
|
||||||
|
"$DIST_DIR"/latest.yml "$DIST_DIR"/latest-linux.yml "$DIST_DIR"/latest-mac.yml; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
bn=$(basename "$f")
|
||||||
|
if should_skip_gitea_upload "$bn"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
upload_paths+=("$f")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#upload_paths[@]}" -eq 0 ]; then
|
||||||
|
echo "No launcher files to upload under $DIST_DIR (expected Fractured-Launcher*.exe, *.AppImage, latest.yml, latest-linux.yml)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in "${upload_paths[@]}"; do
|
||||||
|
bn=$(basename "$f")
|
||||||
|
echo "Uploading $bn …"
|
||||||
|
curl -fsS -X POST "${AUTH_H[@]}" \
|
||||||
|
-F "attachment=@${f}" \
|
||||||
|
"$API/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${rel_id}/assets"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done. Release $TAG (id=$rel_id): replaced ${#upload_paths[@]} launcher file(s); game assets left intact."
|
||||||
Reference in New Issue
Block a user