Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da17074a63 | |||
| b8826370c6 | |||
| d1d68cb44a |
@@ -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).
|
||||
@@ -6,6 +6,9 @@ AzerothCore. Upstream AzerothCore does not ship these paths.
|
||||
|
||||
Contents:
|
||||
- BUILD-NATIVE.md — fork-specific native build notes (moved from repo root).
|
||||
- BALANCE-TODO.md — open balance / scaling questions raised by play-testers
|
||||
that have not yet been actioned (e.g. Feral Cat scaling). Knock items off
|
||||
as they ship.
|
||||
- CLAUDE.md — optional AI assistant context (moved from repo root).
|
||||
- CLIENT-PATCHES.md — what ships in a Fractured client release (MPQs +
|
||||
patched Wow.exe), where to download them (Releases page), and how
|
||||
|
||||
@@ -584,6 +584,37 @@ void RevokeBlockedSpellsForPlayer(Player* pl)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Same migration for meta-skill cascade spells. Earlier builds
|
||||
// (and this one until just now) revoked the rune-enchant spells
|
||||
// (Razorice, Cinderglacier, Rune of the Fallen Crusader, ...)
|
||||
// when a Paragon learned Runeforging via the panel, because
|
||||
// they're active spells and the default classifier treats
|
||||
// unknown active cascades as leaks. New policy: anything on
|
||||
// SKILL_RUNEFORGING is part of the Runeforging meta-skill
|
||||
// package and stays. Drop the revoked row and, if we have a
|
||||
// still-owned parent (typically Runeforging itself, 53428),
|
||||
// re-record as a child so refund/unlearn still cleans them up.
|
||||
bool isMetaSkillRevoke = false;
|
||||
{
|
||||
auto bounds = sSpellMgr->GetSkillLineAbilityMapBounds(sid);
|
||||
for (auto it = bounds.first; it != bounds.second; ++it)
|
||||
{
|
||||
if (it->second->SkillLine == SKILL_RUNEFORGING)
|
||||
{
|
||||
isMetaSkillRevoke = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isMetaSkillRevoke)
|
||||
{
|
||||
if (parent && ownedPanelSpells.count(parent))
|
||||
passiveMigrate.emplace_back(parent, sid);
|
||||
passiveStaleAll.push_back(sid);
|
||||
++migrated;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowed.count(sid))
|
||||
{
|
||||
stale.push_back(sid);
|
||||
@@ -753,11 +784,57 @@ void RevokeBlockedSpellsForPlayer(Player* pl)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allowlist for ACTIVE spells we explicitly want kept as
|
||||
// panel_spell_children, even though the general policy is "actives in
|
||||
// children = legacy garbage, drop them" (see
|
||||
// PruneSkillLineCascadeChildrenFromDb).
|
||||
//
|
||||
// The original kAttached set was 100% passives (Frost Fever, Blood
|
||||
// Plague, Forceful Deflection, Runic Focus). For those, "passive ==
|
||||
// keep" was a perfect proxy. Runeforging changed that: the 8 basic
|
||||
// rune-enchant spells (53344, 53343, 53341, 53331, 53342, 53323,
|
||||
// 54447, 54446) are ACTIVE casts that we DO want to attach to the
|
||||
// Runeforging panel purchase so:
|
||||
// * The Lua-substitute Runeforge UI can cast them (HasActiveSpell).
|
||||
// * Refunding Runeforging cleans them up via the standard
|
||||
// panel_spell_children unlearn path.
|
||||
//
|
||||
// Without this allowlist, PruneSkillLineCascadeChildrenFromDb runs
|
||||
// immediately after PanelLearnSpellChain attaches them, sees them as
|
||||
// non-passive, drops them, and inserts panel_spell_revoked rows --
|
||||
// stranding the player with no usable runeforging menu.
|
||||
//
|
||||
// Every entry here MUST also appear in PanelLearnSpellChain::kAttached
|
||||
// AND in OnPlayerLogin's kFixup list (or a shared source if those ever
|
||||
// get factored out). The pair ordering is (parentHead, attachedSpell),
|
||||
// matching kAttached / kFixup.
|
||||
struct IntentionalActiveAttached { uint32 parent; uint32 child; };
|
||||
static IntentionalActiveAttached const kIntentionalActiveAttached[] = {
|
||||
{ 53428, 53344 }, // Runeforging -> Rune of the Fallen Crusader
|
||||
{ 53428, 53343 }, // Runeforging -> Rune of Razorice
|
||||
{ 53428, 53341 }, // Runeforging -> Rune of Cinderglacier
|
||||
{ 53428, 53331 }, // Runeforging -> Rune of Lichbane
|
||||
{ 53428, 53342 }, // Runeforging -> Rune of Spellshattering
|
||||
{ 53428, 53323 }, // Runeforging -> Rune of Swordshattering
|
||||
{ 53428, 54447 }, // Runeforging -> Rune of Spellbreaking
|
||||
{ 53428, 54446 }, // Runeforging -> Rune of Swordbreaking
|
||||
};
|
||||
|
||||
[[nodiscard]] static bool IsIntentionalActiveAttachedChild(uint32 parent, uint32 child)
|
||||
{
|
||||
for (auto const& e : kIntentionalActiveAttached)
|
||||
if (e.parent == parent && e.child == child)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Current policy: cascade-granted passives stick as panel_spell_children;
|
||||
// only actives get revoked. This pass exists to scrub *legacy* rows that
|
||||
// older logic inserted incorrectly — specifically, any active spell that
|
||||
// ended up in panel_spell_children from a build that classified things
|
||||
// differently. Passive children are always retained.
|
||||
// differently. Passive children are always retained, as are entries
|
||||
// whitelisted via kIntentionalActiveAttached (Runeforging rune-enchants
|
||||
// are active casts that we deliberately attach as children).
|
||||
static void PruneSkillLineCascadeChildrenFromDb(Player* pl, uint32 lowGuid)
|
||||
{
|
||||
if (!pl)
|
||||
@@ -776,6 +853,8 @@ static void PruneSkillLineCascadeChildrenFromDb(Player* pl, uint32 lowGuid)
|
||||
SpellInfo const* info = sSpellMgr->GetSpellInfo(child);
|
||||
if (info && info->IsPassive())
|
||||
continue; // passives always stay
|
||||
if (IsIntentionalActiveAttachedChild(parent, child))
|
||||
continue; // intentional active attachment
|
||||
// Active in children -> legacy garbage. Drop the row, revoke the
|
||||
// spell, and persist into panel_spell_revoked so the login sweep
|
||||
// catches future cascade re-fires.
|
||||
@@ -1228,13 +1307,45 @@ void PanelLearnSpellChain(Player* pl, uint32 baseSpellId)
|
||||
if (!dep)
|
||||
continue;
|
||||
|
||||
if (dep->IsPassive())
|
||||
// Meta-skill cascade carve-out. Runeforging (776) is a
|
||||
// CLASS-category skill that, once granted, is supposed to
|
||||
// cascade ALL its rune-enchant spells (Rune of the Fallen
|
||||
// Crusader, Razorice, Cinderglacier, Lichbane, Spell-/
|
||||
// Sword-shattering, Spell-/Sword-breaking, Stoneskin
|
||||
// Gargoyle, Nerubian Carapace) for the player to choose
|
||||
// from at a runeforge anvil. Those rune-enchants are
|
||||
// ACTIVE spells, so the default policy below would
|
||||
// revoke them and the player would learn Runeforging
|
||||
// for nothing. Treat the whole cluster the same way we
|
||||
// treat passive deps: persist as children of the panel
|
||||
// purchase so refund/unlearn drops them too, but do NOT
|
||||
// revoke them.
|
||||
//
|
||||
// Detection: walk the dep's own SkillLineAbility entries
|
||||
// and check for SKILL_RUNEFORGING. This auto-handles all
|
||||
// 10 rune-enchant spells without an ID-by-ID allowlist.
|
||||
bool isMetaSkillCascade = false;
|
||||
{
|
||||
auto bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spellId);
|
||||
for (auto it = bounds.first; it != bounds.second; ++it)
|
||||
{
|
||||
if (it->second->SkillLine == SKILL_RUNEFORGING)
|
||||
{
|
||||
isMetaSkillCascade = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dep->IsPassive() || isMetaSkillCascade)
|
||||
{
|
||||
DbInsertPanelSpellChild(lowGuid, trackId, spellId);
|
||||
if (diag)
|
||||
LOG_INFO("module",
|
||||
"[paragon-diag] +{} (passive dep, kept as child of {})",
|
||||
spellId, trackId);
|
||||
"[paragon-diag] +{} ({} dep, kept as child of {})",
|
||||
spellId,
|
||||
isMetaSkillCascade ? "meta-skill" : "passive",
|
||||
trackId);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1301,6 +1412,34 @@ void PanelLearnSpellChain(Player* pl, uint32 baseSpellId)
|
||||
{ 45477, 59921 }, // Icy Touch -> Frost Fever (passive entry)
|
||||
{ 45477, 61455 }, // Icy Touch -> Runic Focus (parry from spell power)
|
||||
{ 45902, 49410 }, // Blood Strike -> Forceful Deflection (parry from strength)
|
||||
|
||||
// Runeforging -> 8 basic rune-enchants. The
|
||||
// SkillLineAbility rows for these (skill 776) all ship
|
||||
// with AcquireMethod = 0 in the DBC (i.e. NOT auto-learn-
|
||||
// on-skill-grant). For stock DKs the engine's hardcoded
|
||||
// runeforging UI hand-rolls the cast for whichever rune
|
||||
// the player picks, but for our Lua-substitute UI the
|
||||
// server's HandleCastSpellOpcode / HasActiveSpell gate
|
||||
// rejects the cast unless the spell is in the spellbook.
|
||||
// Force-attach them as panel children so:
|
||||
// 1. The player actually owns the spells (cast works).
|
||||
// 2. Refunding Runeforging cleans them up via the
|
||||
// standard panel_spell_children unlearn path.
|
||||
// The two ADVANCED runes (Stoneskin Gargoyle 62158 and
|
||||
// Nerubian Carapace 70164) are intentionally NOT listed:
|
||||
// retail gates them behind item drops from heroic
|
||||
// dungeons / Naxx / ICC, and our SkillLineAbility rows
|
||||
// for them already use AcquireMethod=0 so the player
|
||||
// gets them when they pick up the appropriate item, not
|
||||
// for free with Runeforging itself.
|
||||
{ 53428, 53344 }, // Runeforging -> Rune of the Fallen Crusader
|
||||
{ 53428, 53343 }, // Runeforging -> Rune of Razorice
|
||||
{ 53428, 53341 }, // Runeforging -> Rune of Cinderglacier
|
||||
{ 53428, 53331 }, // Runeforging -> Rune of Lichbane
|
||||
{ 53428, 53342 }, // Runeforging -> Rune of Spellshattering
|
||||
{ 53428, 53323 }, // Runeforging -> Rune of Swordshattering
|
||||
{ 53428, 54447 }, // Runeforging -> Rune of Spellbreaking
|
||||
{ 53428, 54446 }, // Runeforging -> Rune of Swordbreaking
|
||||
};
|
||||
|
||||
// Self-heal: a previous build of mod-paragon (briefly shipped)
|
||||
@@ -2256,9 +2395,21 @@ void PushSpellSnapshot(Player* pl)
|
||||
continue;
|
||||
}
|
||||
|
||||
SpellInfo const* info = sSpellMgr->GetSpellInfo(sid);
|
||||
if (info && info->HasAttribute(SPELL_ATTR0_DO_NOT_DISPLAY))
|
||||
continue;
|
||||
// Note: we deliberately do NOT filter SPELL_ATTR0_DO_NOT_DISPLAY
|
||||
// here. Earlier builds did, on the theory that hidden spells
|
||||
// shouldn't appear in the spellbook-style Overview tab. That
|
||||
// turned out to be wrong: cascade-granted hidden passives
|
||||
// (Forceful Deflection, Frost Fever, ...) live in
|
||||
// panel_spell_children, not in panel_spells -- so the only
|
||||
// entries that ever land in this query are the chain heads
|
||||
// the player explicitly purchased. Those MUST appear in the
|
||||
// Overview even if their DBC entry is hidden, because they
|
||||
// are the player's actual purchases (e.g. Runeforging 53428
|
||||
// is hidden in the DBC but is the entire Runeforging panel
|
||||
// purchase). Filtering them out left chars whose only buy
|
||||
// was Runeforging with an empty Overview tab -- looked like
|
||||
// a regression but was actually the existing snapshot logic
|
||||
// mismatching the panel's user-facing semantics.
|
||||
|
||||
std::string token = (first ? "" : ",") + std::to_string(sid);
|
||||
if (buf.size() + token.size() > kSnapshotChunkBudget)
|
||||
@@ -4018,6 +4169,24 @@ public:
|
||||
{ 45477, 59921 }, // Icy Touch -> Frost Fever (passive)
|
||||
{ 45477, 61455 }, // Icy Touch -> Runic Focus
|
||||
{ 45902, 49410 }, // Blood Strike -> Forceful Deflection
|
||||
|
||||
// Runeforging -> 8 basic rune-enchants. Mirror of
|
||||
// PanelLearnSpellChain::kAttached: the SLA rows for
|
||||
// these (skill 776) ship with AcquireMethod=0 so the
|
||||
// engine's normal cascade never grants them, and for
|
||||
// the substitute Lua runeforging UI to actually be
|
||||
// able to cast them HasActiveSpell needs to return
|
||||
// true. Existing Paragon characters that bought
|
||||
// Runeforging before this fix landed get them
|
||||
// retro-granted on their next login.
|
||||
{ 53428, 53344 }, // Runeforging -> Fallen Crusader
|
||||
{ 53428, 53343 }, // Runeforging -> Razorice
|
||||
{ 53428, 53341 }, // Runeforging -> Cinderglacier
|
||||
{ 53428, 53331 }, // Runeforging -> Lichbane
|
||||
{ 53428, 53342 }, // Runeforging -> Spellshattering
|
||||
{ 53428, 53323 }, // Runeforging -> Swordshattering
|
||||
{ 53428, 54447 }, // Runeforging -> Spellbreaking
|
||||
{ 53428, 54446 }, // Runeforging -> Swordbreaking
|
||||
};
|
||||
for (auto const& lf : kFixup)
|
||||
{
|
||||
|
||||
@@ -7,13 +7,15 @@
|
||||
# AZEROTH_BIN=/path/to/azeroth-server/bin bash scripts/start-azeroth-servers.sh
|
||||
#
|
||||
# Environment:
|
||||
# AZEROTH_BIN — directory with authserver and worldserver (default: /root/azeroth-server/bin)
|
||||
# AZEROTH_BIN — directory with authserver and worldserver (default: /home/fractured-panel/azeroth-server/bin)
|
||||
# AZEROTH_LOG_DIR — log directory (default: <parent of bin>/logs)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BIN_DIR="${AZEROTH_BIN:-/root/azeroth-server/bin}"
|
||||
LOG_DIR="${AZEROTH_LOG_DIR:-$(cd "$(dirname "$BIN_DIR")" && pwd)/logs}"
|
||||
BIN_DIR="${AZEROTH_BIN:-/home/fractured-panel/azeroth-server/bin}"
|
||||
BASE_DIR="$(cd "$(dirname "$BIN_DIR")" && pwd)"
|
||||
LOG_DIR="${AZEROTH_LOG_DIR:-${BASE_DIR}/logs}"
|
||||
CONF_DIR="${BASE_DIR}/etc"
|
||||
|
||||
AUTH_BIN="${BIN_DIR}/authserver"
|
||||
WORLD_BIN="${BIN_DIR}/worldserver"
|
||||
@@ -35,15 +37,16 @@ mkdir -p "$LOG_DIR"
|
||||
|
||||
cd "$BIN_DIR"
|
||||
|
||||
nohup "$AUTH_BIN" >>"$LOG_DIR/authserver.log" 2>&1 &
|
||||
nohup "$AUTH_BIN" -c "${CONF_DIR}/authserver.conf" >>"$LOG_DIR/authserver.log" 2>&1 &
|
||||
disown || true
|
||||
|
||||
sleep 2
|
||||
|
||||
nohup "$WORLD_BIN" >>"$LOG_DIR/worldserver.log" 2>&1 &
|
||||
nohup "$WORLD_BIN" -c "${CONF_DIR}/worldserver.conf" >>"$LOG_DIR/worldserver.log" 2>&1 &
|
||||
disown || true
|
||||
|
||||
echo "Started authserver and worldserver (survives SSH disconnect)."
|
||||
echo "Bin: $BIN_DIR"
|
||||
echo "Config: $CONF_DIR"
|
||||
echo "Logs: $LOG_DIR/authserver.log"
|
||||
echo " $LOG_DIR/worldserver.log"
|
||||
|
||||
@@ -35,6 +35,7 @@ FULL_BUILD=0
|
||||
COMPILE_ONLY=0
|
||||
DRY_RUN=0
|
||||
DO_RUN_AFTER=0
|
||||
INSTALL_PREFIX=""
|
||||
POST_UPDATE_CMD="${FRACTURED_POST_UPDATE_CMD:-}"
|
||||
GIT_REMOTE="${FRACTURED_GIT_REMOTE:-origin}"
|
||||
|
||||
@@ -49,6 +50,7 @@ Options:
|
||||
--no-pull Skip git pull (only compile current tree).
|
||||
--full ./acore.sh compiler all (clean + configure + compile).
|
||||
--compile-only ./acore.sh compiler compile (incremental).
|
||||
--prefix PATH Override CMAKE_INSTALL_PREFIX (updates conf/config.sh BINPATH).
|
||||
--dry-run Print commands without running them.
|
||||
--run-after [CMD] Run shell command after successful compile. If CMD is omitted,
|
||||
uses FRACTURED_POST_UPDATE_CMD from the environment.
|
||||
@@ -87,6 +89,15 @@ while [[ $# -gt 0 ]]; do
|
||||
COMPILE_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--prefix)
|
||||
shift
|
||||
if [[ $# -eq 0 || "$1" == -* ]]; then
|
||||
echo "error: --prefix requires a path argument" >&2
|
||||
exit 2
|
||||
fi
|
||||
INSTALL_PREFIX="$1"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
@@ -129,6 +140,16 @@ fi
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
if [[ -n "$INSTALL_PREFIX" ]]; then
|
||||
echo "==> updating conf/config.sh BINPATH to: $INSTALL_PREFIX"
|
||||
if grep -q '^BINPATH=' conf/config.sh; then
|
||||
run sed -i "s|^BINPATH=.*|BINPATH=\"$INSTALL_PREFIX\"|" conf/config.sh
|
||||
else
|
||||
echo "BINPATH=\"$INSTALL_PREFIX\"" >> conf/config.sh
|
||||
fi
|
||||
export BINPATH="$INSTALL_PREFIX"
|
||||
fi
|
||||
|
||||
if [[ "$DO_RUN_AFTER" -eq 1 && -z "${POST_UPDATE_CMD// }" ]]; then
|
||||
echo "error: --run-after needs a command or FRACTURED_POST_UPDATE_CMD set in the environment." >&2
|
||||
exit 2
|
||||
|
||||
@@ -12048,7 +12048,20 @@ void Player::learnSkillRewardedSpells(uint32 skill_id, uint32 skill_value)
|
||||
// weapon, language, and racial skill cascades stay enabled so things
|
||||
// like recipe auto-learn, weapon proficiencies, and racial perks
|
||||
// still work.
|
||||
if (getClass() == CLASS_PARAGON)
|
||||
//
|
||||
// Carve-out: SKILL_RUNEFORGING (776) is a CLASS-category skill but
|
||||
// behaves like a profession in this context — the player buys ONE
|
||||
// panel ability (Runeforging, spell 53428) and the rune-enchant
|
||||
// spells (Rune of the Fallen Crusader, Razorice, Cinderglacier, ...)
|
||||
// are supposed to come along for the ride via the standard SLA
|
||||
// cascade, exactly the same way they do for a stock DK. Without
|
||||
// this carve-out, the early-return below blocks the cascade and a
|
||||
// Paragon who buys Runeforging gets the skill but no actual rune
|
||||
// options at the runeforge anvil. The cascade only fires once per
|
||||
// skill-grant for 776 (it's not on UpdateSkillsForLevel) so the
|
||||
// "leaking back into the spellbook" concern that motivates the
|
||||
// early-return doesn't apply to this skill.
|
||||
if (getClass() == CLASS_PARAGON && skill_id != SKILL_RUNEFORGING)
|
||||
{
|
||||
if (SkillLineEntry const* sl = sSkillLineStore.LookupEntry(skill_id))
|
||||
if (sl->categoryId == SKILL_CATEGORY_CLASS)
|
||||
|
||||
@@ -474,7 +474,25 @@ void Player::UpdateAttackPowerAndDamage(bool ranged)
|
||||
switch (GetShapeshiftForm())
|
||||
{
|
||||
case FORM_CAT:
|
||||
val2 = (GetLevel() * mLevelMult) + GetStat(STAT_STRENGTH) * 2.0f + GetStat(STAT_AGILITY) - 20.0f + weapon_bonus + m_baseFeralAP;
|
||||
// Fractured: Cat Form gets 2 AP per Agility instead of stock 1.
|
||||
// Field reports said "weapons dont automatically feature feral
|
||||
// AP on this server and nothing is currently rescaled, super
|
||||
// low feral scale" -- specifically a CAT issue, not a bear
|
||||
// issue (the resident bear had 11k AP, the resident cat was
|
||||
// miles behind because Stam > AP and Armor > AP for bears
|
||||
// hides the missing weapon-AP for them but cat's whole
|
||||
// mainline is melee crits scaling off AP). The cleanest knob
|
||||
// that does NOT touch bear is the AGI multiplier in this
|
||||
// switch -- bears get STR*2 with no AGI term, so doubling
|
||||
// the AGI coefficient lifts cat's primary scaling stat
|
||||
// without re-buffing bear. Also pairs with the cat-form
|
||||
// Master Shapeshifter buff in SpellAuraEffects.cpp's
|
||||
// FORM_CAT branch (bp doubled there). Together that lands
|
||||
// the resident Feral expert's recommendation
|
||||
// ("instead of adding a new passive, you could probably
|
||||
// just increase Cat Form's Master Shapeshifter value along
|
||||
// with its tooltip, alongside buffing the agi scaling").
|
||||
val2 = (GetLevel() * mLevelMult) + GetStat(STAT_STRENGTH) * 2.0f + GetStat(STAT_AGILITY) * 2.0f - 20.0f + weapon_bonus + m_baseFeralAP;
|
||||
break;
|
||||
case FORM_BEAR:
|
||||
case FORM_DIREBEAR:
|
||||
|
||||
@@ -126,6 +126,89 @@ bool IsParagonWeaponSubclassWildcardSpell(uint32 spellId)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// -- 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
|
||||
|
||||
// -- Hunter Aspects (combat). Like presences, these are regular
|
||||
// auras stock AC, not engine-shapeshifts; rank-1 ids cover all
|
||||
// ranks via GetFirstRankSpell. Cheetah / Pack are the utility
|
||||
// aspects -- included per design decision so a hunter must pick
|
||||
// between Hawk and Cheetah (no more "always Hawk while running",
|
||||
// matches Ascension's nerf rationale for Monkey).
|
||||
case 13165: // Aspect of the Hawk (rank 1; ranks 14318/14319/14320/14321/14322/25296/27044 covered via first-rank)
|
||||
case 5118: // Aspect of the Cheetah
|
||||
case 13159: // Aspect of the Pack (rank 1; rank 27047 covered via first-rank)
|
||||
case 20043: // Aspect of the Wild (rank 1; ranks 20190/27045 covered via first-rank)
|
||||
case 13161: // Aspect of the Beast
|
||||
case 13163: // Aspect of the Monkey
|
||||
case 34074: // Aspect of the Viper
|
||||
case 61846: // Aspect of the Dragonhawk (rank 1; rank 61847 covered via first-rank)
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
float baseMoveSpeed[MAX_MOVE_TYPE] =
|
||||
{
|
||||
2.5f, // MOVE_WALK
|
||||
|
||||
@@ -2300,6 +2300,34 @@ private:
|
||||
// so all ranks of a talent are covered by listing the rank-1 id.
|
||||
[[nodiscard]] bool IsParagonWeaponSubclassWildcardSpell(uint32 spellId);
|
||||
|
||||
// Fractured: returns true iff `spellId` is one of the cross-class
|
||||
// "stance-like" auras that we treat as mutually exclusive on this server,
|
||||
// regardless of the caster's class. Stock AC engine-shapeshifts (warrior
|
||||
// stances, druid forms, Shadowform, Metamorphosis) already auto-replace
|
||||
// each other via `RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT)` in
|
||||
// `AuraEffect::HandleAuraModShapeshift`, but DK Presences and Hunter
|
||||
// Aspects are regular auras (the client just renders them in the stance
|
||||
// bar), so they coexist with shapeshifts in stock AC. The Fractured rule
|
||||
// makes the entire union mutually exclusive: warrior stances + druid
|
||||
// forms (combat AND utility -- Travel/Aquatic/Flight/Swift Flight) +
|
||||
// Ghost Wolf + Stealth + Shadowform + Metamorphosis + DK Presences +
|
||||
// Hunter Aspects (combat AND utility -- Cheetah/Pack). Casting any of
|
||||
// them removes any other from the same set, so e.g. a Paragon DK can no
|
||||
// longer stack Frost Presence on top of Bear Form, and a hunter must
|
||||
// pick between Hawk and Cheetah even out of combat.
|
||||
//
|
||||
// The set is matched against `SpellInfo::GetFirstRankSpell()`'s id so
|
||||
// every rank of every aspect / form is covered by listing the rank-1 id.
|
||||
// Server-wide -- this is *not* gated on CLASS_PARAGON because the only
|
||||
// stock-class-only effect of the rule (a DK losing Travel Form when
|
||||
// they cast Frost Presence) is impossible: stock DKs cannot shapeshift.
|
||||
// Used by `Aura::CanStackWith` to refuse stacking, which then drives
|
||||
// `Unit::_RemoveNoStackAurasDueToAura` to drop the older aura -- the
|
||||
// same mechanism Battle Elixirs / Curses already use (spell_group rule
|
||||
// SPELL_GROUP_STACK_RULE_EXCLUSIVE), implemented in C++ here so we do
|
||||
// not have to enumerate every rank of every aspect / form in SQL.
|
||||
[[nodiscard]] bool IsFracturedExclusiveStanceSpell(uint32 spellId);
|
||||
|
||||
namespace Acore
|
||||
{
|
||||
// Binary predicate for sorting Units based on percent value of a power
|
||||
|
||||
@@ -1545,7 +1545,21 @@ void AuraEffect::HandleShapeshiftBoosts(Unit* target, bool apply) const
|
||||
// Master Shapeshifter - Cat
|
||||
if (AuraEffect const* aurEff = target->GetDummyAuraEffect(SPELLFAMILY_GENERIC, 2851, 0))
|
||||
{
|
||||
int32 bp = aurEff->GetAmount();
|
||||
// Fractured: cat-only Master Shapeshifter bonus is
|
||||
// doubled (rank 1: 2% -> 4%, rank 2: 4% -> 8%) to
|
||||
// make Feral Cat builds feel less "super low feral
|
||||
// scale" without touching bear / moonkin / tree (the
|
||||
// FORM_BEAR / FORM_MOONKIN / FORM_TREE branches
|
||||
// below pass `bp` straight through, unchanged). The
|
||||
// talent's own SpellInfo Effects[].BasePoints is
|
||||
// intentionally NOT bumped -- aurEff->GetAmount()
|
||||
// returns the per-rank talent value (2 / 4) shared
|
||||
// across all four forms, so we apply the cat
|
||||
// multiplier here at the cast site, leaving every
|
||||
// other form on the stock value. Pairs with the
|
||||
// Cat-Form AGI doubling in StatSystem.cpp's
|
||||
// UpdateAttackPowerAndDamage FORM_CAT branch.
|
||||
int32 bp = aurEff->GetAmount() * 2;
|
||||
target->CastCustomSpell(target, 48420, &bp, nullptr, nullptr, true);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1973,6 +1973,31 @@ bool Aura::CanStackWith(Aura const* existingAura) const
|
||||
|| (sameCaster && m_spellInfo->IsAuraExclusiveBySpecificPerCasterWith(existingSpellInfo)))
|
||||
return false;
|
||||
|
||||
// Fractured: cross-class stance / form / presence / aspect exclusivity.
|
||||
// Stock AC's engine-shapeshift removal in HandleAuraModShapeshift only
|
||||
// covers warrior stances, druid forms, Shadowform, Metamorphosis, etc.
|
||||
// -- DK Presences and Hunter Aspects are regular auras (the client just
|
||||
// happens to render them in the stance bar) and therefore stack with
|
||||
// engine-shapeshifts in stock AC. The Fractured rule (server-wide --
|
||||
// see IsFracturedExclusiveStanceSpell in Unit.h for the curated set
|
||||
// and the design rationale) treats the union of stances + forms (combat
|
||||
// AND utility) + presences + aspects as mutually exclusive. Refusing
|
||||
// to stack here triggers the same _RemoveNoStackAurasDueToAura cleanup
|
||||
// path that Battle Elixirs / Curses already use, so the older aura
|
||||
// drops and the newly-cast one applies cleanly. Different ranks of the
|
||||
// same talent (e.g. Hawk rank 4 -> Hawk rank 7) are NOT treated as
|
||||
// exclusive with each other -- IsFracturedExclusiveStanceSpell resolves
|
||||
// to first-rank ids, so we compare those.
|
||||
if (IsFracturedExclusiveStanceSpell(m_spellInfo->Id) && IsFracturedExclusiveStanceSpell(existingSpellInfo->Id))
|
||||
{
|
||||
SpellInfo const* newFirst = m_spellInfo->GetFirstRankSpell();
|
||||
SpellInfo const* oldFirst = existingSpellInfo->GetFirstRankSpell();
|
||||
uint32 newFirstId = newFirst ? newFirst->Id : m_spellInfo->Id;
|
||||
uint32 oldFirstId = oldFirst ? oldFirst->Id : existingSpellInfo->Id;
|
||||
if (newFirstId != oldFirstId)
|
||||
return false;
|
||||
}
|
||||
|
||||
// check spell group stack rules
|
||||
switch (sSpellMgr->CheckSpellGroupStackRules(m_spellInfo, existingSpellInfo))
|
||||
{
|
||||
|
||||
@@ -7732,56 +7732,64 @@ SpellCastResult Spell::CheckItems(uint32* param1, uint32* param2)
|
||||
switch (pItem->GetTemplate()->SubClass)
|
||||
{
|
||||
case ITEM_SUBCLASS_WEAPON_THROWN:
|
||||
{
|
||||
uint32 ammo = pItem->GetEntry();
|
||||
if (!m_caster->ToPlayer()->HasItemCount(ammo))
|
||||
return SPELL_FAILED_NO_AMMO;
|
||||
};
|
||||
{
|
||||
// Fractured: thrown abilities behave like DK runes -- they
|
||||
// remain usable even when the player has run out of the
|
||||
// throwing item. Stock AC returned SPELL_FAILED_NO_AMMO
|
||||
// here; we just drop the gate. Spell::TakeAmmo's stack
|
||||
// decrement is wrapped in a HasItemCount check via
|
||||
// DestroyItemCount and will silently no-op at zero. The
|
||||
// ranged-DPS bonus naturally vanishes when the stack runs
|
||||
// out, so the player still throws but loses the per-shot
|
||||
// damage contribution from the throwing item.
|
||||
break;
|
||||
};
|
||||
case ITEM_SUBCLASS_WEAPON_GUN:
|
||||
case ITEM_SUBCLASS_WEAPON_BOW:
|
||||
case ITEM_SUBCLASS_WEAPON_CROSSBOW:
|
||||
{
|
||||
uint32 ammo = m_caster->ToPlayer()->GetUInt32Value(PLAYER_AMMO_ID);
|
||||
if (!ammo)
|
||||
{
|
||||
// Requires No Ammo
|
||||
if (m_caster->HasAura(46699))
|
||||
break; // skip other checks
|
||||
|
||||
return SPELL_FAILED_NO_AMMO;
|
||||
}
|
||||
|
||||
ItemTemplate const* ammoProto = sObjectMgr->GetItemTemplate(ammo);
|
||||
if (!ammoProto)
|
||||
return SPELL_FAILED_NO_AMMO;
|
||||
|
||||
if (ammoProto->Class != ITEM_CLASS_PROJECTILE)
|
||||
return SPELL_FAILED_NO_AMMO;
|
||||
|
||||
// check ammo ws. weapon compatibility
|
||||
switch (pItem->GetTemplate()->SubClass)
|
||||
{
|
||||
case ITEM_SUBCLASS_WEAPON_BOW:
|
||||
case ITEM_SUBCLASS_WEAPON_CROSSBOW:
|
||||
if (ammoProto->SubClass != ITEM_SUBCLASS_ARROW)
|
||||
return SPELL_FAILED_NO_AMMO;
|
||||
break;
|
||||
case ITEM_SUBCLASS_WEAPON_GUN:
|
||||
if (ammoProto->SubClass != ITEM_SUBCLASS_BULLET)
|
||||
return SPELL_FAILED_NO_AMMO;
|
||||
break;
|
||||
default:
|
||||
return SPELL_FAILED_NO_AMMO;
|
||||
}
|
||||
|
||||
if (!m_caster->ToPlayer()->HasItemCount(ammo))
|
||||
{
|
||||
m_caster->ToPlayer()->SetUInt32Value(PLAYER_AMMO_ID, 0);
|
||||
return SPELL_FAILED_NO_AMMO;
|
||||
}
|
||||
};
|
||||
{
|
||||
// Fractured: ranged abilities behave like DK runes -- they
|
||||
// remain usable when the player has no ammo loaded or the
|
||||
// quiver / pouch is empty. The DPS-bonus path (StatSystem.cpp:
|
||||
// `weaponMin/MaxDamage += GetAmmoDPS() * attackSpeedMod`)
|
||||
// reads `m_ammoDPS`, which is 0 when no ammo is loaded and
|
||||
// recomputed via Player::_ApplyAmmoBonuses on equip / stack
|
||||
// exhaustion, so a hunter with an empty bag still casts
|
||||
// Steady Shot / Aimed Shot etc. -- they just lose the arrow
|
||||
// / bullet DPS contribution.
|
||||
//
|
||||
// We deliberately do NOT clear PLAYER_AMMO_ID when the bag
|
||||
// empties. Defense in depth alongside the data-side fix:
|
||||
//
|
||||
// * The primary client-side fix lives in Spell.dbc --
|
||||
// SpellInfoCorrections.cpp's "drop EquippedItemClass on
|
||||
// hunter shot abilities" block (mirrored client-side by
|
||||
// fractured-tooling/_patch_spell_dbc_hunter_ammo.py)
|
||||
// sets EquippedItemClass = -1 on every player-castable
|
||||
// hunter shot, which removes the 3.3.5a client's
|
||||
// "ranged weapon AND ammo slot non-empty" preflight
|
||||
// gate entirely. After that, ammo is purely a
|
||||
// server-side DPS bonus, never a hard requirement.
|
||||
//
|
||||
// * Keeping the (now-stale) ammo id in PLAYER_AMMO_ID
|
||||
// field is harmless: TakeAmmo's DestroyItemCount
|
||||
// silently no-ops when HasItemCount is 0, and
|
||||
// _ApplyAmmoBonuses already recomputes m_ammoDPS to 0
|
||||
// when the proto can no longer be found / the stack is
|
||||
// empty. So the StatSystem.cpp ammo-DPS path gracefully
|
||||
// degrades to "no bonus" the moment the bag goes empty.
|
||||
//
|
||||
// * Player un-equipping the ammo via the paper-doll
|
||||
// right-click still routes through RemoveAmmo() and
|
||||
// zeroes the field -- that is the player's explicit
|
||||
// action and we leave it alone.
|
||||
//
|
||||
// Net result: hunter has bow + ammo -> full DPS; bow only ->
|
||||
// shots still fire, no ammo DPS; no bow -> client engine's
|
||||
// own ranged-weapon gate still blocks (Auto Shot timer
|
||||
// simply never spins up without a ranged weapon equipped).
|
||||
break;
|
||||
};
|
||||
case ITEM_SUBCLASS_WEAPON_WAND:
|
||||
break;
|
||||
default:
|
||||
@@ -7852,6 +7860,36 @@ SpellCastResult Spell::CheckSpellFocus()
|
||||
// check spell focus object
|
||||
if (m_spellInfo->RequiresSpellFocus)
|
||||
{
|
||||
// Fractured / Paragon: skip the GO proximity check for Paragon
|
||||
// casters when the spell is a runeforge enchant (skill line 776).
|
||||
// Paragons get a Runeforge tab in the Character Advancement
|
||||
// panel that lets them apply rune-enchants from anywhere in the
|
||||
// world -- no need to fly back to Acherus or find the nearest
|
||||
// Eastern Plaguelands anvil. The skill-line gate keeps the
|
||||
// bypass tightly scoped: only the 10 SkillLineAbility-tagged
|
||||
// rune-enchant spells qualify, every other spell that uses
|
||||
// SpellFocusObject (Enchanting bench, Cooking fire, Lockpicking
|
||||
// anvil, etc.) keeps its requirement intact.
|
||||
//
|
||||
// DK / other class casters are unchanged -- this carve-out
|
||||
// intentionally checks getClass() == CLASS_PARAGON rather than
|
||||
// IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_ABILITY) (which
|
||||
// would also return true for Paragons via mod-paragon's hook).
|
||||
// Stock DK behavior must stay vanilla; the QoL bypass is a
|
||||
// class-12 feature only.
|
||||
if (m_caster && m_caster->IsPlayer()
|
||||
&& m_caster->ToPlayer()->getClass() == CLASS_PARAGON)
|
||||
{
|
||||
auto bounds = sSpellMgr->GetSkillLineAbilityMapBounds(m_spellInfo->Id);
|
||||
for (auto it = bounds.first; it != bounds.second; ++it)
|
||||
{
|
||||
if (it->second->SkillLine == SKILL_RUNEFORGING)
|
||||
{
|
||||
return SPELL_CAST_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CellCoord p(Acore::ComputeCellCoord(m_caster->GetPositionX(), m_caster->GetPositionY()));
|
||||
Cell cell(p);
|
||||
|
||||
|
||||
@@ -1395,15 +1395,16 @@ bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod, Unit const* liste
|
||||
{
|
||||
case 53817: // Shaman: Maelstrom Weapon
|
||||
{
|
||||
// Allow any rank of Mage Fireball / Frostbolt / Arcane Blast to
|
||||
// benefit from the cast-time + cost reduction spellmod.
|
||||
// Allow any rank of Mage Fireball / Frostbolt to benefit from
|
||||
// the cast-time + cost reduction spellmod. Arcane Blast was on
|
||||
// the allowlist briefly but proved too potent stacked with its
|
||||
// own self-buff -- removed.
|
||||
if (SpellFamilyName == SPELLFAMILY_MAGE)
|
||||
{
|
||||
SpellInfo const* first = GetFirstRankSpell();
|
||||
uint32 firstId = first ? first->Id : Id;
|
||||
if (firstId == 133 /*Fireball*/
|
||||
|| firstId == 116 /*Frostbolt*/
|
||||
|| firstId == 30451 /*Arcane Blast*/)
|
||||
|| firstId == 116 /*Frostbolt*/)
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "DBCStores.h"
|
||||
#include "DBCStructure.h"
|
||||
#include "GameGraveyard.h"
|
||||
#include "ItemTemplate.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "SpellMgr.h"
|
||||
|
||||
@@ -5380,6 +5381,118 @@ void SpellMgr::LoadSpellInfoCorrections()
|
||||
spellInfo->Attributes |= SPELL_ATTR0_PASSIVE;
|
||||
});
|
||||
|
||||
// Fractured: move Death Knight Presences and Hunter Aspects out of
|
||||
// SpellCategory 47 ("Combat States") so they cancel/toggle the same
|
||||
// way Druid shapeshift forms do.
|
||||
//
|
||||
// Category 47 is the "stance bar" category. The 3.3.5a client UI
|
||||
// explicitly disables right-click-cancel and `/cancelaura <name>` for
|
||||
// any aura whose Spell.dbc Category column points at a SpellCategory
|
||||
// entry that is "Combat States" (47). Druid forms (Bear Form, Cat
|
||||
// Form, Travel Form, Moonkin, Tree of Life, etc.) sit in Category 0
|
||||
// and are therefore freely cancellable -- right-click drops the form,
|
||||
// /cancelaura drops it, recasting from the action bar drops it.
|
||||
// Warrior stances, DK Presences and Hunter Aspects all live in
|
||||
// Category 47, which is why none of them are cancellable in stock.
|
||||
//
|
||||
// For the cross-class stance / form / presence / aspect exclusivity
|
||||
// rule (see IsFracturedExclusiveStanceSpell in Unit.cpp), a Paragon
|
||||
// hybrid often wants to drop their active presence/aspect so they can
|
||||
// apply a different stance/form *without* first switching to a
|
||||
// different presence/aspect. Setting Category to 0 here mirrors what
|
||||
// Druid forms already do, gives the cancel/toggle UX the user
|
||||
// explicitly asked for, and -- importantly -- does NOT change the
|
||||
// action bar (presences and aspects are not engine-shapeshifts, the
|
||||
// bar swap behavior is owned by SPELL_AURA_MOD_SHAPESHIFT, not by
|
||||
// SpellCategory). The matching client-side Spell.dbc edit ships in
|
||||
// patch-enUS-4.MPQ via _patch_spell_dbc_presences_cancelable.py.
|
||||
//
|
||||
// Warrior stances are also included per design decision 2026-05-11
|
||||
// ("you could make Warrior Stances toggleable as well, it should be
|
||||
// okay"). The previously-shipped Stances=0 client patch already lets
|
||||
// Paragon non-warriors cast every warrior ability without picking up
|
||||
// a stance, so a stock warrior who right-clicks their stance just
|
||||
// ends up at "no stance" -- which on this server still leaves all
|
||||
// their warrior abilities available. Stock warriors who like the
|
||||
// never-cancel UX can simply not right-click; nothing forces them.
|
||||
//
|
||||
// Tradeoff: stances / presences / aspects lose the 1s SpellCategory
|
||||
// GCD that Category 47 enforces between same-category spells. This
|
||||
// matches the Druid-form UX (Bear -> Cat -> Bear has no shared GCD),
|
||||
// and the cross-class exclusivity rule in Aura::CanStackWith already
|
||||
// prevents stacking, so the only thing actually possible at "0s GCD"
|
||||
// is rapid-toggling the same stance on and off, which is harmless.
|
||||
ApplySpellFix({
|
||||
// Warrior Stances.
|
||||
2457, // Battle Stance
|
||||
71, // Defensive Stance
|
||||
2458, // Berserker Stance
|
||||
|
||||
// Death Knight Presences.
|
||||
48266, // Blood Presence
|
||||
48263, // Frost Presence
|
||||
48265, // Unholy Presence
|
||||
|
||||
// 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
|
||||
48266, // Blood Presence
|
||||
48263, // Frost Presence
|
||||
48265, // Unholy Presence
|
||||
}, [](SpellInfo* spellInfo)
|
||||
{
|
||||
spellInfo->AttributesEx6 &= ~SPELL_ATTR6_ALLOW_WHILE_RIDING_VEHICLE;
|
||||
});
|
||||
|
||||
// Fractured: strip reagent requirements from every player-class spell at
|
||||
// load time. Filtered by SpellFamilyName != 0 so that profession spells
|
||||
// (cooking, alchemy, enchanting, blacksmithing, jewelcrafting, leatherworking,
|
||||
@@ -5418,6 +5531,76 @@ void SpellMgr::LoadSpellInfoCorrections()
|
||||
LOG_INFO("server.loading", ">> Fractured: cleared reagents on {} class spells", fixedClassSpells);
|
||||
}
|
||||
|
||||
// Fractured: drop EquippedItemClass on hunter shot abilities at load time
|
||||
// so the server agrees with the matching client-side Spell.dbc patch
|
||||
// (fractured-tooling/_patch_spell_dbc_hunter_ammo.py). Both surfaces have
|
||||
// to agree -- if only the client patch shipped, the server's stock
|
||||
// EquippedItemClass check would still reject mid-cast; if only the server
|
||||
// mirror shipped, the 3.3.5a client preflight would still block the cast
|
||||
// packet from leaving the box with "Ammo needs to be in the paper doll
|
||||
// ammo slot before it can be fired." The Spell::CheckCast soft-fail
|
||||
// (Spell.cpp 7741..) and the never-clear-PLAYER_AMMO_ID change there are
|
||||
// still in place as defense in depth so a half-deployed client / server
|
||||
// pair degrades to the soft-fail behavior rather than to hard rejects.
|
||||
//
|
||||
// Filter mirrors the Python patcher byte-for-byte:
|
||||
// SpellFamilyName == SPELLFAMILY_HUNTER (9)
|
||||
// AND EquippedItemClass == ITEM_CLASS_WEAPON (2)
|
||||
// AND EquippedItemSubClassMask & ((1<<BOW)|(1<<GUN)|(1<<XBOW)) != 0
|
||||
// with a small DENYLIST of item-equip-driven passive auras (Quiver /
|
||||
// Ammo Pouch haste ranks, Legendary Bow Haste, Aynasha's Bow proc) whose
|
||||
// entire purpose is "have a ranged weapon equipped" -- those keep their
|
||||
// stock EquippedItemClass = 2.
|
||||
//
|
||||
// Effect: after this fix, hunter shots leave the client preflight without
|
||||
// hitting the ammo-slot gate AND pass the server's EquippedItemClass
|
||||
// check unconditionally. _ApplyAmmoBonuses still gates the arrow / bullet
|
||||
// DPS bonus on actually having a stack in the quiver, so equipping ammo
|
||||
// continues to give the DPS bump and an empty quiver no longer bricks
|
||||
// abilities -- "you still get the DPS increase from arrows but aren't
|
||||
// completely neutered if you run out", per the resident hunter expert.
|
||||
{
|
||||
constexpr uint32 RANGED_SUBCLASS_MASK =
|
||||
(1u << ITEM_SUBCLASS_WEAPON_BOW)
|
||||
| (1u << ITEM_SUBCLASS_WEAPON_GUN)
|
||||
| (1u << ITEM_SUBCLASS_WEAPON_CROSSBOW);
|
||||
|
||||
// Keep in sync with DENYLIST in
|
||||
// fractured-tooling/_patch_spell_dbc_hunter_ammo.py.
|
||||
static const std::unordered_set<uint32> hunterAmmoDenylist = {
|
||||
// Quiver / Ammo Pouch ranged-attack-speed haste passives (gun).
|
||||
14824, 14825, 14826, 14827, 14828, 14829,
|
||||
// Quiver passive haste (bow + crossbow).
|
||||
29413, 29414, 29415, 29416, 29417, 29418,
|
||||
// Late-rank quiver haste, gun-only.
|
||||
44333,
|
||||
// Legendary Bow Haste (item proc on a specific bow).
|
||||
44972,
|
||||
// Aynasha's Bow item proc.
|
||||
19767,
|
||||
};
|
||||
|
||||
uint32 fixedShots = 0;
|
||||
for (uint32 spellId = 1; spellId < sSpellMgr->GetSpellInfoStoreSize(); ++spellId)
|
||||
{
|
||||
SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId);
|
||||
if (!info || info->SpellFamilyName != SPELLFAMILY_HUNTER)
|
||||
continue;
|
||||
if (info->EquippedItemClass != ITEM_CLASS_WEAPON)
|
||||
continue;
|
||||
if (info->EquippedItemSubClassMask <= 0
|
||||
|| (uint32(info->EquippedItemSubClassMask) & RANGED_SUBCLASS_MASK) == 0)
|
||||
continue;
|
||||
if (hunterAmmoDenylist.find(spellId) != hunterAmmoDenylist.end())
|
||||
continue;
|
||||
|
||||
SpellInfo* mut = const_cast<SpellInfo*>(info);
|
||||
mut->EquippedItemClass = -1;
|
||||
++fixedShots;
|
||||
}
|
||||
LOG_INFO("server.loading", ">> Fractured: dropped EquippedItemClass on {} hunter shot abilities", fixedShots);
|
||||
}
|
||||
|
||||
LOG_INFO("server.loading", ">> Loading spell dbc data corrections in {} ms", GetMSTimeDiffToNow(oldMSTime));
|
||||
LOG_INFO("server.loading", " ");
|
||||
}
|
||||
|
||||
@@ -664,6 +664,23 @@ class spell_dk_dancing_rune_weapon : public AuraScript
|
||||
return false;
|
||||
|
||||
SpellInfo const* spellInfo = eventInfo.GetSpellInfo();
|
||||
|
||||
// Fractured / Paragon: Paragon owners get a "ghostly weapon copies
|
||||
// your swings" identity instead of the stock magical-doppelganger
|
||||
// (which also re-cast Death Coil / Icy Touch / Howling Blast /
|
||||
// etc.). For Paragon callers only, accept auto-attacks and
|
||||
// melee-class abilities (Hamstring, Sinister Strike, Heart Strike,
|
||||
// Frost Strike, Mortal Strike, ...) and reject magic / ranged
|
||||
// spells. Stock DK gating below is left untouched.
|
||||
if (IsParagonWildcardCaller(eventInfo.GetActor()))
|
||||
{
|
||||
if (!eventInfo.GetDamageInfo())
|
||||
return false;
|
||||
if (spellInfo && spellInfo->DmgClass != SPELL_DAMAGE_CLASS_MELEE)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!spellInfo)
|
||||
return true;
|
||||
|
||||
@@ -1916,6 +1933,20 @@ class spell_dk_pestilence : public SpellScript
|
||||
if (!target)
|
||||
return;
|
||||
|
||||
// Fractured / Paragon: when the Pestilence caster is a Paragon and
|
||||
// wildcard family matching is on, also spread (or refresh) Priest
|
||||
// Devouring Plague. Devouring Plague's Dispel field is DISPEL_DISEASE
|
||||
// and Unit::GetDiseasesByCaster already counts it for Paragon callers
|
||||
// (see Unit.cpp), so it is conceptually a disease; stock Pestilence
|
||||
// just hard-codes Blood Plague + Frost Fever and so silently drops it.
|
||||
// GetAuraOfRankedSpell with the rank-1 id (2944) 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 the GetAuraOfRankedSpell will return
|
||||
// null for them and this branch is a no-op there.
|
||||
bool const paragonSpread = IsParagonWildcardCaller(caster);
|
||||
|
||||
// Spread on others
|
||||
if (target != hitUnit)
|
||||
{
|
||||
@@ -1926,6 +1957,11 @@ class spell_dk_pestilence : public SpellScript
|
||||
// Frost Fever
|
||||
if (target->GetAura(SPELL_DK_FROST_FEVER, caster->GetGUID()))
|
||||
caster->CastSpell(hitUnit, SPELL_DK_FROST_FEVER, true);
|
||||
|
||||
// Fractured / Paragon: Devouring Plague spread.
|
||||
if (paragonSpread)
|
||||
if (Aura const* dp = target->GetAuraOfRankedSpell(2944 /* Devouring Plague r1 */, caster->GetGUID()))
|
||||
caster->CastSpell(hitUnit, dp->GetId(), true);
|
||||
}
|
||||
// Refresh on target
|
||||
else if (caster->GetAura(SPELL_DK_GLYPH_OF_DISEASE))
|
||||
@@ -1946,6 +1982,11 @@ class spell_dk_pestilence : public SpellScript
|
||||
disease->RefreshDuration();
|
||||
else if (Aura* disease = target->GetAuraOfRankedSpell(SPELL_DK_CRYPT_FEVER_R1, caster->GetGUID()))
|
||||
disease->RefreshDuration();
|
||||
|
||||
// Fractured / Paragon: Devouring Plague Glyph-of-Disease refresh.
|
||||
if (paragonSpread)
|
||||
if (Aura* dp = target->GetAuraOfRankedSpell(2944 /* Devouring Plague r1 */, caster->GetGUID()))
|
||||
dp->RefreshDuration();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1793,8 +1793,10 @@ class spell_sha_maelstrom_weapon : public AuraScript
|
||||
// Fractured / Paragon: spell_proc row 53817 is data-relaxed (wildcard
|
||||
// family/mask) so cross-class spells can reach this CheckProc. We
|
||||
// restore the original Shaman gating here for stock callers and add
|
||||
// the Paragon-only Mage Fireball / Frostbolt / Arcane Blast allowlist
|
||||
// mirroring the IsAffectedBySpellMod hook in SpellInfo.cpp.
|
||||
// the Paragon-only Mage Fireball / Frostbolt allowlist mirroring the
|
||||
// IsAffectedBySpellMod hook in SpellInfo.cpp. Arcane Blast was on the
|
||||
// allowlist briefly but proved too potent stacked with its own
|
||||
// self-buff -- removed.
|
||||
bool CheckProc(ProcEventInfo& eventInfo)
|
||||
{
|
||||
SpellInfo const* procSpell = eventInfo.GetSpellInfo();
|
||||
@@ -1820,8 +1822,7 @@ class spell_sha_maelstrom_weapon : public AuraScript
|
||||
SpellInfo const* first = procSpell->GetFirstRankSpell();
|
||||
uint32 firstId = first ? first->Id : procSpell->Id;
|
||||
if (firstId == 133 /*Fireball*/
|
||||
|| firstId == 116 /*Frostbolt*/
|
||||
|| firstId == 30451 /*Arcane Blast*/)
|
||||
|| firstId == 116 /*Frostbolt*/)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user