mirror of
https://gitcode.com/GitHub_Trending/az/azerothcore-wotlk.git
synced 2026-10-10 07:06:38 +08:00
fix(Spells/Paladin): Apply Beacon target healing bonuses (#27772)
This commit is contained in:
+1
-1
@@ -147,7 +147,7 @@ go test -tags=e2e ./local/... -count=1 -v -timeout 30m -parallel 1
|
||||
| combat/pets | summon / GUID / attack / dismiss | P1 | covered; dungeon Raise Dead `blocked-harness` (ready-check / instance summon) | #27081 |
|
||||
| combat/threat | engage / taunt switch / kill clears combat | P1 | covered | — |
|
||||
| combat/vehicles | spellclick steed enter/exit | P2 | covered | — |
|
||||
| spells/aura | apply/query; CC broken by damage; mount persist; paladin same-aura per-caster + Aura Mastery | P1 | covered (`TestAC_26130_*`, `TestAC_25765_*`) | #26130 #25765 |
|
||||
| spells/aura | apply/query; CC broken by damage; mount persist; paladin same-aura per-caster + Aura Mastery; Beacon applies its target's Improved Devotion Aura healing bonus exactly once for Holy Light, Flash of Light, Holy Shock and Lay on Hands | P1 | covered (`TestAC_26130_*`, `TestAC_25765_*`, `TestAC_27756_*`) | #26130 #25765 #27756 |
|
||||
| spells/cast | Charge on dummy; fail path; stance; Raise Dead + ghoul | P1 | covered (`TestAC_27061_*`) | #27061 |
|
||||
| spells/effects | Charge / grounding totem / Sweeping Strikes Execute; forced cast summons at the forced caster, not at the unit that forced it; a tripped Gordunni Trap rolls out both dirt mounds | P1 | covered (`TestAC_26997_*`, `TestEffects_ForceCastDestination`, `TestEffects_GordunniTrapRollsBothMounds`); dummy-summon `blocked-harness` (engineering dummy lifetime) | #26774 #26997 #27621 |
|
||||
| social/group | form / leave / leader / loot method / disband | P2 | covered | — |
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
//go:build e2e
|
||||
|
||||
package aura_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/azerothcore/AzerothGhost/e2e/e2eharness"
|
||||
"github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta"
|
||||
)
|
||||
|
||||
const (
|
||||
spellBeaconOfLight = uint32(53563)
|
||||
spellLightsBeacon = uint32(53651)
|
||||
spellBeaconHolyLight = uint32(53652)
|
||||
spellBeaconFlashOfLight = uint32(53653)
|
||||
spellBeaconHolyShock = uint32(53654)
|
||||
spellHolyLight = uint32(48782)
|
||||
spellFlashOfLight = uint32(48785)
|
||||
spellHolyShock = uint32(48825)
|
||||
spellHolyShockHealing = uint32(48821)
|
||||
spellLayOnHands = uint32(48788)
|
||||
spellDevotionAura = uint32(48942)
|
||||
spellImprovedDevotionAura = uint32(20140)
|
||||
spellImprovedDevotionEffect = uint32(63514)
|
||||
|
||||
smsgSpellHealLog = uint16(0x0150)
|
||||
healingBonusPct = uint32(6)
|
||||
testMaxHealth = uint32(200_000)
|
||||
)
|
||||
|
||||
type spellHealLog struct {
|
||||
Target uint64
|
||||
Caster uint64
|
||||
SpellID uint32
|
||||
Heal uint32
|
||||
Overheal uint32
|
||||
}
|
||||
|
||||
type beaconHealCase struct {
|
||||
name string
|
||||
castSpell uint32
|
||||
sourceHealSpell uint32
|
||||
beaconHealSpell uint32
|
||||
}
|
||||
|
||||
// Issue: https://github.com/azerothcore/azerothcore-wotlk/issues/27756
|
||||
//
|
||||
// Improved Devotion Aura gives grouped targets 6% more healing received. The
|
||||
// ungrouped recipient supplies the target-without-bonus case without depending
|
||||
// on range or terrain. Healing the grouped paladin supplies the both-targets-
|
||||
// have-the-bonus case. Beacon must use its target's bonus exactly once for all
|
||||
// four source heals.
|
||||
func TestAC_27756_BeaconUsesTargetHealingBonusOnce(t *testing.T) {
|
||||
meta.Begin(t, meta.TestMeta{
|
||||
Tags: []string{"med", "spells", "issue", "multi_bot"},
|
||||
Runtime: "med",
|
||||
Issue: 27756,
|
||||
Category: "spells/aura",
|
||||
})
|
||||
|
||||
bots := e2eharness.NewScenario(t, e2eharness.ScenarioOpts{
|
||||
Prefix: "Bcn277",
|
||||
Bots: []e2eharness.BotSpec{
|
||||
{Role: "healer", Race: e2eharness.RaceHuman, Class: e2eharness.ClassPaladin, Level: 80},
|
||||
{Role: "beacon", Race: e2eharness.RaceHuman, Class: e2eharness.ClassPaladin, Level: 80},
|
||||
{Role: "recipient", Race: e2eharness.RaceHuman, Class: e2eharness.ClassWarrior, Level: 80},
|
||||
},
|
||||
})
|
||||
healer := e2eharness.ByRole(t, bots, "healer")
|
||||
beacon := e2eharness.ByRole(t, bots, "beacon")
|
||||
recipient := e2eharness.ByRole(t, bots, "recipient")
|
||||
|
||||
pad := e2eharness.PackagePad(t)
|
||||
recipient.TeleportPad(t, pad)
|
||||
e2eharness.FormPartyAtPad(t, pad, healer, beacon)
|
||||
defer e2eharness.DisbandParty(t, healer, beacon)
|
||||
if recipient.InGroup() {
|
||||
e2eharness.Preconditionf(t, "recipient unexpectedly joined the healer and Beacon target's party")
|
||||
}
|
||||
|
||||
for _, spellID := range []uint32{spellBeaconOfLight, spellHolyLight, spellFlashOfLight, spellHolyShock, spellLayOnHands} {
|
||||
healer.Learn(t, spellID)
|
||||
}
|
||||
beacon.Learn(t, spellImprovedDevotionAura)
|
||||
beacon.Learn(t, spellDevotionAura)
|
||||
|
||||
for _, bot := range bots {
|
||||
e2eharness.CombatReady(t, bot.World, e2eharness.CombatReadyOpts{Power: true})
|
||||
}
|
||||
healer.GM(t, ".cheat cooldown on")
|
||||
healer.FlushWorld(t)
|
||||
|
||||
beacon.CastMust(t, spellDevotionAura, 0, 10*time.Second)
|
||||
healer.WaitUnitAura(t, beacon.GUID, spellImprovedDevotionEffect, 10*time.Second)
|
||||
healer.WaitUnitAura(t, healer.GUID, spellImprovedDevotionEffect, 10*time.Second)
|
||||
if recipient.HasAura(spellImprovedDevotionEffect) {
|
||||
e2eharness.Preconditionf(t, "ungrouped recipient unexpectedly has Improved Devotion Aura effect %d", spellImprovedDevotionEffect)
|
||||
}
|
||||
|
||||
healLogs := make(chan spellHealLog, 64)
|
||||
cancelHealLogs := healer.World.AddPacketHook(func(opcode uint16, data []byte) {
|
||||
if opcode != smsgSpellHealLog {
|
||||
return
|
||||
}
|
||||
if event, ok := parseSpellHealLog(data); ok {
|
||||
select {
|
||||
case healLogs <- event:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})
|
||||
defer cancelHealLogs()
|
||||
|
||||
cases := []beaconHealCase{
|
||||
{name: "Holy Light", castSpell: spellHolyLight, sourceHealSpell: spellHolyLight, beaconHealSpell: spellBeaconHolyLight},
|
||||
{name: "Flash of Light", castSpell: spellFlashOfLight, sourceHealSpell: spellFlashOfLight, beaconHealSpell: spellBeaconFlashOfLight},
|
||||
{name: "Holy Shock", castSpell: spellHolyShock, sourceHealSpell: spellHolyShockHealing, beaconHealSpell: spellBeaconHolyShock},
|
||||
{name: "Lay on Hands", castSpell: spellLayOnHands, sourceHealSpell: spellLayOnHands, beaconHealSpell: spellBeaconHolyShock},
|
||||
}
|
||||
|
||||
matrices := []struct {
|
||||
name string
|
||||
source *e2eharness.ScenarioBot
|
||||
wantBeaconHealing func(uint32) uint32
|
||||
}{
|
||||
{
|
||||
name: "Beacon target bonus only",
|
||||
source: recipient,
|
||||
wantBeaconHealing: func(sourceHeal uint32) uint32 {
|
||||
return sourceHeal + sourceHeal*healingBonusPct/100
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Both targets have the bonus",
|
||||
source: healer,
|
||||
wantBeaconHealing: func(sourceHeal uint32) uint32 {
|
||||
return sourceHeal
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, matrix := range matrices {
|
||||
t.Run(matrix.name, func(t *testing.T) {
|
||||
wantSourceBonus := matrix.source == healer
|
||||
if got := matrix.source.HasAura(spellImprovedDevotionEffect); got != wantSourceBonus {
|
||||
e2eharness.Preconditionf(t, "%s Improved Devotion Aura effect=%v, want %v", matrix.source.Name, got, wantSourceBonus)
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
prepareInjuredPlayer(t, matrix.source)
|
||||
prepareInjuredPlayer(t, beacon)
|
||||
drainHealLogs(healLogs)
|
||||
|
||||
healer.CastMust(t, spellBeaconOfLight, beacon.GUID, 10*time.Second)
|
||||
healer.WaitUnitAura(t, beacon.GUID, spellBeaconOfLight, 10*time.Second)
|
||||
healer.WaitUnitAura(t, healer.GUID, spellLightsBeacon, 10*time.Second)
|
||||
drainHealLogs(healLogs)
|
||||
|
||||
healer.CastMust(t, testCase.castSpell, matrix.source.GUID, 10*time.Second)
|
||||
sourceLog, beaconLog := waitForBeaconHealPair(t, healLogs, matrix.source.GUID,
|
||||
testCase.sourceHealSpell, beacon.GUID, testCase.beaconHealSpell, 10*time.Second)
|
||||
if sourceLog.Heal == 0 || beaconLog.Heal == 0 {
|
||||
e2eharness.ConfirmedBugf(t, 27756, "%s produced zero healing source=%d Beacon=%d",
|
||||
testCase.name, sourceLog.Heal, beaconLog.Heal)
|
||||
}
|
||||
|
||||
want := matrix.wantBeaconHealing(sourceLog.Heal)
|
||||
if !withinOne(beaconLog.Heal, want) {
|
||||
e2eharness.ConfirmedBugf(t, 27756,
|
||||
"%s: source healed %d but Beacon healed %d, want %d (source overheal=%d, Beacon overheal=%d)",
|
||||
testCase.name, sourceLog.Heal, beaconLog.Heal, want, sourceLog.Overheal, beaconLog.Overheal)
|
||||
}
|
||||
t.Logf("PASS %s source=%d Beacon=%d want=%d", testCase.name, sourceLog.Heal, beaconLog.Heal, want)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func prepareInjuredPlayer(t *testing.T, bot *e2eharness.ScenarioBot) {
|
||||
t.Helper()
|
||||
if err := bot.World.SetTarget(bot.GUID); err != nil {
|
||||
e2eharness.Preconditionf(t, "%s select self before health setup: %v", bot.Name, err)
|
||||
}
|
||||
bot.GM(t, fmt.Sprintf(".modify hp %d", testMaxHealth))
|
||||
bot.FlushWorld(t)
|
||||
waitForSelfHealth(t, bot, func(health, maxHealth uint32) bool {
|
||||
return health == testMaxHealth && maxHealth == testMaxHealth
|
||||
}, "full health after .modify hp")
|
||||
|
||||
bot.Damage(t, bot.GUID, testMaxHealth/2)
|
||||
waitForSelfHealth(t, bot, func(health, maxHealth uint32) bool {
|
||||
return health > 0 && health < maxHealth
|
||||
}, "damage before healing")
|
||||
}
|
||||
|
||||
func waitForSelfHealth(t *testing.T, bot *e2eharness.ScenarioBot, ready func(uint32, uint32) bool, state string) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
health, maxHealth := bot.World.Health(), bot.World.MaxHealth()
|
||||
if ready(health, maxHealth) {
|
||||
return
|
||||
}
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
}
|
||||
e2eharness.Preconditionf(t, "%s never reached %s (health=%d/%d)", bot.Name, state, bot.World.Health(), bot.World.MaxHealth())
|
||||
}
|
||||
|
||||
func waitForBeaconHealPair(t *testing.T, logs <-chan spellHealLog, sourceTarget uint64, sourceSpell uint32,
|
||||
beaconTarget uint64, beaconSpell uint32, timeout time.Duration) (spellHealLog, spellHealLog) {
|
||||
t.Helper()
|
||||
deadline := time.NewTimer(timeout)
|
||||
defer deadline.Stop()
|
||||
|
||||
var sourceLog, beaconLog spellHealLog
|
||||
for sourceLog.Heal == 0 || beaconLog.Heal == 0 {
|
||||
select {
|
||||
case event := <-logs:
|
||||
switch {
|
||||
case event.Target == sourceTarget && event.SpellID == sourceSpell:
|
||||
sourceLog = event
|
||||
case event.Target == beaconTarget && event.SpellID == beaconSpell:
|
||||
beaconLog = event
|
||||
}
|
||||
case <-deadline.C:
|
||||
e2eharness.ConfirmedBugf(t, 27756,
|
||||
"missing heal logs source(target=0x%X spell=%d heal=%d) Beacon(target=0x%X spell=%d heal=%d)",
|
||||
sourceTarget, sourceSpell, sourceLog.Heal, beaconTarget, beaconSpell, beaconLog.Heal)
|
||||
}
|
||||
}
|
||||
return sourceLog, beaconLog
|
||||
}
|
||||
|
||||
func drainHealLogs(logs <-chan spellHealLog) {
|
||||
for {
|
||||
select {
|
||||
case <-logs:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseSpellHealLog(data []byte) (spellHealLog, bool) {
|
||||
reader := bytes.NewReader(data)
|
||||
target, ok := readPackedGUID(reader)
|
||||
if !ok {
|
||||
return spellHealLog{}, false
|
||||
}
|
||||
caster, ok := readPackedGUID(reader)
|
||||
if !ok {
|
||||
return spellHealLog{}, false
|
||||
}
|
||||
|
||||
event := spellHealLog{Target: target, Caster: caster}
|
||||
if binary.Read(reader, binary.LittleEndian, &event.SpellID) != nil ||
|
||||
binary.Read(reader, binary.LittleEndian, &event.Heal) != nil ||
|
||||
binary.Read(reader, binary.LittleEndian, &event.Overheal) != nil {
|
||||
return spellHealLog{}, false
|
||||
}
|
||||
return event, true
|
||||
}
|
||||
|
||||
func readPackedGUID(reader *bytes.Reader) (uint64, bool) {
|
||||
mask, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var guid uint64
|
||||
for index := uint(0); index < 8; index++ {
|
||||
if mask&(1<<index) == 0 {
|
||||
continue
|
||||
}
|
||||
value, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
guid |= uint64(value) << (index * 8)
|
||||
}
|
||||
return guid, true
|
||||
}
|
||||
|
||||
func withinOne(got, want uint32) bool {
|
||||
if got > want {
|
||||
return got-want <= 1
|
||||
}
|
||||
return want-got <= 1
|
||||
}
|
||||
@@ -606,16 +606,21 @@ void SpellMgr::LoadSpellInfoCorrections()
|
||||
spellInfo->AttributesEx3 |= SPELL_ATTR3_SUPPRESS_TARGET_PROCS;
|
||||
});
|
||||
|
||||
ApplySpellFix({
|
||||
54968, // Glyph of Holy Light, Damage Class should be magic
|
||||
53652, // Beacon of Light heal, Damage Class should be magic
|
||||
53654
|
||||
}, [](SpellInfo* spellInfo)
|
||||
// Glyph of Holy Light
|
||||
ApplySpellFix({ 54968 }, [](SpellInfo* spellInfo)
|
||||
{
|
||||
spellInfo->AttributesEx3 |= SPELL_ATTR3_IGNORE_CASTER_MODIFIERS;
|
||||
spellInfo->DmgClass = SPELL_DAMAGE_CLASS_MAGIC;
|
||||
});
|
||||
|
||||
// Beacon of Light
|
||||
ApplySpellFix({ 53652, 53653, 53654 }, [](SpellInfo* spellInfo)
|
||||
{
|
||||
spellInfo->AttributesEx3 |= SPELL_ATTR3_IGNORE_CASTER_MODIFIERS;
|
||||
spellInfo->AttributesEx6 &= ~SPELL_ATTR6_IGNORE_HEALTH_MODIFIERS;
|
||||
spellInfo->DmgClass = SPELL_DAMAGE_CLASS_MAGIC;
|
||||
});
|
||||
|
||||
// Wild Hunt
|
||||
ApplySpellFix({ 62758, 62762 }, [](SpellInfo* spellInfo)
|
||||
{
|
||||
|
||||
@@ -139,6 +139,7 @@ enum PaladinProcSpells
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_HL = 53652,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_FOL = 53653,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_HS = 53654,
|
||||
SPELL_PALADIN_LAY_ON_HANDS_R1 = 633,
|
||||
SPELL_PALADIN_HOLY_LIGHT_R1 = 635,
|
||||
SPELL_PALADIN_FLASH_OF_LIGHT_R1 = 19750,
|
||||
SPELL_PALADIN_GLYPH_OF_HOLY_LIGHT_HEAL = 54968,
|
||||
@@ -152,6 +153,7 @@ enum PaladinProcSpells
|
||||
SPELL_PALADIN_HOLY_POWER_SPELL_POWER = 28793,
|
||||
SPELL_PALADIN_HOLY_POWER_MP5 = 28795,
|
||||
SPELL_PALADIN_HOLY_MENDING = 64891,
|
||||
SPELL_PALADIN_GLYPH_OF_DIVINITY = 54939,
|
||||
SPELL_PALADIN_GLYPH_OF_DIVINITY_PROC = 54986,
|
||||
SPELL_PALADIN_HEART_OF_THE_CRUSADER_EFF_R1 = 21183,
|
||||
SPELL_PALADIN_JUDGEMENTS_OF_THE_JUST_PROC = 68055,
|
||||
@@ -1102,7 +1104,8 @@ class spell_pal_lay_on_hands : public SpellScript
|
||||
caster->CastSpell(caster, SPELL_PALADIN_IMMUNE_SHIELD_MARKER, true);
|
||||
}
|
||||
// Xinef: Glyph of Divinity
|
||||
else if (target && caster->HasAura(54939) && GetSpellInfo()->Id != 633 && _manaAmount > 0) // excluding first rank
|
||||
else if (target && caster->HasAura(SPELL_PALADIN_GLYPH_OF_DIVINITY) &&
|
||||
GetSpellInfo()->Id != SPELL_PALADIN_LAY_ON_HANDS_R1 && _manaAmount > 0) // excluding first rank
|
||||
{
|
||||
_manaAmount = target->GetPower(POWER_MANA) - _manaAmount;
|
||||
if (_manaAmount > 0)
|
||||
@@ -2220,7 +2223,8 @@ class spell_pal_light_s_beacon : public AuraScript
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_FOL,
|
||||
SPELL_PALADIN_BEACON_OF_LIGHT_HS,
|
||||
SPELL_PALADIN_HOLY_LIGHT_R1,
|
||||
SPELL_PALADIN_FLASH_OF_LIGHT_R1
|
||||
SPELL_PALADIN_FLASH_OF_LIGHT_R1,
|
||||
SPELL_PALADIN_LAY_ON_HANDS_R1
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2252,8 +2256,9 @@ class spell_pal_light_s_beacon : public AuraScript
|
||||
else
|
||||
healSpellId = SPELL_PALADIN_BEACON_OF_LIGHT_HS;
|
||||
|
||||
// Use heal amount before target-specific modifiers to avoid copying them
|
||||
uint32 healAmount = healInfo->GetHealBeforeTakenMods();
|
||||
// Lay on Hands applies target modifiers before HealInfo is built, so use its unmodified base heal.
|
||||
uint32 healAmount = procSpell->IsRankOf(sSpellMgr->AssertSpellInfo(SPELL_PALADIN_LAY_ON_HANDS_R1)) ?
|
||||
eventInfo.GetActor()->GetMaxHealth() : healInfo->GetHealBeforeTakenMods();
|
||||
int32 heal = CalculatePct(healAmount, aurEff->GetAmount());
|
||||
|
||||
Unit* beaconTarget = GetCaster();
|
||||
|
||||
Reference in New Issue
Block a user