Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a1f8eec89 | |||
| 0bb6b0ef84 | |||
| 295cb6df52 | |||
| fbd6ea47f2 | |||
| a64279ed7e | |||
| 87219cb4eb | |||
| da17074a63 | |||
| b8826370c6 | |||
| d1d68cb44a | |||
| 999f7e94bd |
@@ -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:
|
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,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,14 +7143,16 @@ bool Player::CheckAttackFitToAuraRequirement(WeaponAttackType attackType, AuraEf
|
|||||||
if (spellInfo->EquippedItemClass == -1)
|
if (spellInfo->EquippedItemClass == -1)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Fractured / Paragon: cross-class wildcard relaxes weapon-class subclass
|
// Fractured / Paragon: cross-class wildcard relaxes the weapon-subclass
|
||||||
// gates on per-swing proc matches. A Paragon's talent list spans every
|
// gate ONLY for the curated allowlist of cross-class proc talents
|
||||||
// class so a stock weapon-subclass mask (e.g. Maelstrom Weapon's
|
// (currently just Maelstrom Weapon 51528-51532). Weapon-specialization
|
||||||
// axe/mace/staff/fist/dagger restriction) excludes weapons the player
|
// talents like Sword Specialization, Mace Specialization, Hack and
|
||||||
// can legitimately wield. Accept any equipped weapon in attackType slot
|
// Slash, Two-Handed Weapon Specialization etc. deliberately stay
|
||||||
// when listener is a Paragon AND the spell gates on ITEM_CLASS_WEAPON;
|
// weapon-gated for Paragon -- the player picks a weapon and the
|
||||||
// ITEM_CLASS_ARMOR (shield) gates still enforce the original mask.
|
// matching specialization passive activates, same as any class.
|
||||||
if (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON && IsParagonWildcardCaller(this))
|
if (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON
|
||||||
|
&& IsParagonWildcardCaller(this)
|
||||||
|
&& IsParagonWeaponSubclassWildcardSpell(spellInfo->Id))
|
||||||
return GetWeaponForAttack(attackType, true) != nullptr;
|
return GetWeaponForAttack(attackType, true) != nullptr;
|
||||||
|
|
||||||
Item* item = GetWeaponForAttack(attackType, true);
|
Item* item = GetWeaponForAttack(attackType, true);
|
||||||
@@ -12046,7 +12048,20 @@ void Player::learnSkillRewardedSpells(uint32 skill_id, uint32 skill_value)
|
|||||||
// weapon, language, and racial skill cascades stay enabled so things
|
// 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)
|
||||||
@@ -12591,15 +12606,19 @@ 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 weapon-class subclass
|
// Fractured / Paragon: cross-class wildcard relaxes the weapon-subclass
|
||||||
// gates so passive talent auras (e.g. Maelstrom Weapon talents 51528-51532)
|
// gate ONLY for the curated allowlist of cross-class proc talents
|
||||||
// attach for any equipped weapon, not just the talent's restrictive
|
// (currently just Maelstrom Weapon 51528-51532) so the passive talent
|
||||||
// subclass mask. Mirrors CheckAttackFitToAuraRequirement so per-swing
|
// aura attaches when the player wields a non-stock weapon. Weapon-
|
||||||
// proc match agrees with talent-attach time. Still requires *some* weapon
|
// specialization talents (Sword/Mace Specialization, Hack and Slash,
|
||||||
// to be equipped (otherwise unarmed Paragons would auto-activate every
|
// Two-Handed Weapon Specialization, etc.) deliberately stay weapon-
|
||||||
// weapon-gated talent in the game). ITEM_CLASS_ARMOR (shield) is left
|
// gated -- they're meant to bind to a specific weapon type. Still
|
||||||
// alone -- shield-gated talents still need an actual shield.
|
// requires *some* weapon equipped (unarmed Paragons don't auto-activate
|
||||||
if (spellInfo->EquippedItemClass == ITEM_CLASS_WEAPON && IsParagonWildcardCaller(this))
|
// 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)
|
for (uint8 i = EQUIPMENT_SLOT_MAINHAND; i < EQUIPMENT_SLOT_TABARD; ++i)
|
||||||
if (Item const* item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, i))
|
if (Item const* item = GetUseableItemByPos(INVENTORY_SLOT_BAG_0, i))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -103,6 +103,122 @@ bool ParagonFamilyMatches(Unit const* listener, uint32 expectedFamily, uint32 ac
|
|||||||
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
|
||||||
@@ -4387,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])
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2287,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;
|
||||||
@@ -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))
|
||||||
{
|
{
|
||||||
@@ -2255,20 +2280,17 @@ uint8 Aura::GetProcEffectMask(AuraApplication* aurApp, ProcEventInfo& eventInfo,
|
|||||||
return 0;
|
return 0;
|
||||||
if (!item->IsFitToSpellRequirements(GetSpellInfo()))
|
if (!item->IsFitToSpellRequirements(GetSpellInfo()))
|
||||||
{
|
{
|
||||||
// Fractured / Paragon: cross-class wildcard relaxes weapon-
|
// Fractured / Paragon: cross-class wildcard relaxes the
|
||||||
// class subclass gates on per-event proc evaluation. This
|
// weapon-subclass gate ONLY for the curated allowlist of
|
||||||
// mirrors Player::CheckAttackFitToAuraRequirement and
|
// cross-class proc talents (currently just Maelstrom Weapon
|
||||||
// Player::HasItemFitToSpellRequirements -- without this
|
// 51528-51532). Weapon-specialization talents (Sword/Mace
|
||||||
// third bypass, the talent attaches (HasItemFit lets it),
|
// Specialization, Hack and Slash, Two-Handed Weapon
|
||||||
// the per-swing match accepts the weapon (CheckAttackFit
|
// Specialization, etc.) deliberately stay weapon-gated for
|
||||||
// lets it), but IsProcTriggeredOnEvent still kills the
|
// Paragon. Restricted to ITEM_CLASS_WEAPON so shield-gated
|
||||||
// proc here for any weapon outside the talent's stock
|
// talents still need an actual shield.
|
||||||
// subclass mask (e.g. Maelstrom Weapon on a Paragon
|
|
||||||
// wielding a 1H sword or polearm). Restricted to
|
|
||||||
// ITEM_CLASS_WEAPON so shield-gated talents still need
|
|
||||||
// an actual shield.
|
|
||||||
if (!(GetSpellInfo()->EquippedItemClass == ITEM_CLASS_WEAPON
|
if (!(GetSpellInfo()->EquippedItemClass == ITEM_CLASS_WEAPON
|
||||||
&& IsParagonWildcardCaller(target)))
|
&& IsParagonWildcardCaller(target)
|
||||||
|
&& IsParagonWeaponSubclassWildcardSpell(GetSpellInfo()->Id)))
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3589,7 +3589,10 @@ SpellCastResult Spell::prepare(SpellCastTargets const* targets, AuraEffect const
|
|||||||
|
|
||||||
// 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())
|
||||||
|
{
|
||||||
|
if (!m_spellInfo->IsCastTimeRidingMountSpell())
|
||||||
{
|
{
|
||||||
// 1. Has casttime, 2. Or doesn't have flag to allow action during channel
|
// 1. Has casttime, 2. Or doesn't have flag to allow action during channel
|
||||||
if (m_casttime || !m_spellInfo->IsActionAllowedChannel())
|
if (m_casttime || !m_spellInfo->IsActionAllowedChannel())
|
||||||
@@ -3599,6 +3602,7 @@ SpellCastResult Spell::prepare(SpellCastTargets const* targets, AuraEffect const
|
|||||||
return SPELL_FAILED_MOVING;
|
return SPELL_FAILED_MOVING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// xinef: if spell have nearby target entry only, do not allow to cast if no targets are found
|
// xinef: if spell have nearby target entry only, do not allow to cast if no targets are found
|
||||||
if (m_CastItem)
|
if (m_CastItem)
|
||||||
@@ -4436,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())
|
||||||
@@ -5815,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
|
||||||
@@ -5841,12 +5851,16 @@ SpellCastResult Spell::CheckCast(bool strict, uint32* /*param1*/, uint32* /*para
|
|||||||
// cancel autorepeat spells if cast start when moving
|
// cancel autorepeat spells if cast start when moving
|
||||||
// (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())
|
||||||
|
{
|
||||||
|
// Fractured: cast-time mount summons may be started while moving.
|
||||||
|
if (!m_spellInfo->IsCastTimeRidingMountSpell())
|
||||||
{
|
{
|
||||||
// skip stuck spell to allow use it in falling case and apply spell limitations at movement
|
// 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) &&
|
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))
|
(IsAutoRepeat() || (m_spellInfo->AuraInterruptFlags & AURA_INTERRUPT_FLAG_NOT_SEATED) != 0))
|
||||||
return SPELL_FAILED_MOVING;
|
return SPELL_FAILED_MOVING;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Vehicle* vehicle = m_caster->GetVehicle();
|
Vehicle* vehicle = m_caster->GetVehicle();
|
||||||
if (vehicle && !HasTriggeredCastFlag(TRIGGERED_IGNORE_CASTER_MOUNTED_OR_ON_VEHICLE))
|
if (vehicle && !HasTriggeredCastFlag(TRIGGERED_IGNORE_CASTER_MOUNTED_OR_ON_VEHICLE))
|
||||||
@@ -7733,55 +7747,63 @@ SpellCastResult Spell::CheckItems(uint32* param1, uint32* param2)
|
|||||||
{
|
{
|
||||||
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
|
||||||
|
// 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;
|
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
break;
|
|
||||||
case ITEM_SUBCLASS_WEAPON_WAND:
|
case ITEM_SUBCLASS_WEAPON_WAND:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -7852,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)
|
||||||
@@ -1395,15 +1404,16 @@ bool SpellInfo::IsAffectedBySpellMod(SpellModifier const* mod, Unit const* liste
|
|||||||
{
|
{
|
||||||
case 53817: // Shaman: Maelstrom Weapon
|
case 53817: // Shaman: Maelstrom Weapon
|
||||||
{
|
{
|
||||||
// Allow any rank of Mage Fireball / Frostbolt / Arcane Blast to
|
// Allow any rank of Mage Fireball / Frostbolt to benefit from
|
||||||
// benefit from the cast-time + cost reduction spellmod.
|
// 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)
|
if (SpellFamilyName == SPELLFAMILY_MAGE)
|
||||||
{
|
{
|
||||||
SpellInfo const* first = GetFirstRankSpell();
|
SpellInfo const* first = GetFirstRankSpell();
|
||||||
uint32 firstId = first ? first->Id : Id;
|
uint32 firstId = first ? first->Id : Id;
|
||||||
if (firstId == 133 /*Fireball*/
|
if (firstId == 133 /*Fireball*/
|
||||||
|| firstId == 116 /*Frostbolt*/
|
|| firstId == 116 /*Frostbolt*/)
|
||||||
|| firstId == 30451 /*Arcane Blast*/)
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2146,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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -1793,8 +1793,10 @@ class spell_sha_maelstrom_weapon : public AuraScript
|
|||||||
// Fractured / Paragon: spell_proc row 53817 is data-relaxed (wildcard
|
// Fractured / Paragon: spell_proc row 53817 is data-relaxed (wildcard
|
||||||
// family/mask) so cross-class spells can reach this CheckProc. We
|
// family/mask) so cross-class spells can reach this CheckProc. We
|
||||||
// restore the original Shaman gating here for stock callers and add
|
// restore the original Shaman gating here for stock callers and add
|
||||||
// the Paragon-only Mage Fireball / Frostbolt / Arcane Blast allowlist
|
// the Paragon-only Mage Fireball / Frostbolt allowlist mirroring the
|
||||||
// mirroring the IsAffectedBySpellMod hook in SpellInfo.cpp.
|
// 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)
|
bool CheckProc(ProcEventInfo& eventInfo)
|
||||||
{
|
{
|
||||||
SpellInfo const* procSpell = eventInfo.GetSpellInfo();
|
SpellInfo const* procSpell = eventInfo.GetSpellInfo();
|
||||||
@@ -1820,8 +1822,7 @@ class spell_sha_maelstrom_weapon : public AuraScript
|
|||||||
SpellInfo const* first = procSpell->GetFirstRankSpell();
|
SpellInfo const* first = procSpell->GetFirstRankSpell();
|
||||||
uint32 firstId = first ? first->Id : procSpell->Id;
|
uint32 firstId = first ? first->Id : procSpell->Id;
|
||||||
if (firstId == 133 /*Fireball*/
|
if (firstId == 133 /*Fireball*/
|
||||||
|| firstId == 116 /*Frostbolt*/
|
|| firstId == 116 /*Frostbolt*/)
|
||||||
|| firstId == 30451 /*Arcane Blast*/)
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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