fix(Scripts/Ulduar): stop Yogg-Saron's Psychosis and Malady of the Mind from picking low Sanity targets (#27628)

Co-authored-by: horn <[email protected]>
This commit is contained in:
Andrew
2026-09-13 05:21:19 -03:00
committed by GitHub
co-authored by horn
parent e39b3f6f28
commit 7409484aa3
3 changed files with 180 additions and 1 deletions
+1 -1
View File
@@ -164,7 +164,7 @@ go test -tags=e2e ./local/... -count=1 -v -timeout 30m -parallel 1
| guild/charter_bank | charter buy+turn-in | P2 | covered | — |
| instances/bind_reset | party tele; ritual summon | P2 | covered; post-reset summon `blocked-harness` (AcceptSummon after reset) | #10708 |
| instances/classic/stratholme | Timmy remains hidden while a relevant Square Scarlet lives, then emerges after the area is clear | P2 | covered (`TestAC_26363_TimmyEmergesAfterSquareCleared`) | #26363 |
| instances/ulduar | named tele; Freya wave interval; a Laughing Skull's Lunatic Gaze stops at the brain room's geometry instead of draining sanity through it; Elder Brightleaf's Unstable Sun Beams despawn on their own timer instead of outliving him, and one lands under the player rather than all on the elder | P2 | covered (`TestAC_27095_*`, `TestAC_27602_*`, `TestUlduar_BrightleafSunBeams*`); Kologarn Charge `blocked-harness` (bridge Z after Charge) | #26266 #27095 #27602 |
| instances/ulduar | named tele; Freya wave interval; a Laughing Skull's Lunatic Gaze stops at the brain room's geometry instead of draining sanity through it; Elder Brightleaf's Unstable Sun Beams despawn on their own timer instead of outliving him, and one lands under the player rather than all on the elder; Psychosis and Malady of the Mind stop picking a player once they are at 40 Sanity or less | P2 | covered (`TestAC_27095_*`, `TestAC_27602_*`, `TestAC_27590_*`, `TestUlduar_BrightleafSunBeams*`); Kologarn Charge `blocked-harness` (bridge Z after Charge) | #26266 #27095 #27590 #27602 |
| world/gameevents | Call to Arms banners at the Dalaran portals belong to the side they stand on, and the already-correct Warsong set is unchanged. **Wants an exclusive realm**: starting a holiday re-anchors its schedule in the running worldserver until restart; holidays already running are left alone | P2 | covered (`TestAC_24380_*`); Shattrath's 23 positions `gap` | #24380 |
---
@@ -525,3 +525,164 @@ func sunBeamGUIDs(beams []sunBeamSnap) []string {
}
return out
}
// Issue: https://github.com/azerothcore/azerothcore-wotlk/issues/27590
// PR: https://github.com/azerothcore/azerothcore-wotlk/pull/27628
// Psychosis (63795) and Malady of the Mind (63830) must skip players sitting at 40 Sanity
// or less. Both pick a random enemy through TARGET_UNIT_SRC_AREA_ENEMY over a 50000 yd
// radius and AzerothCore never filtered that list, so the players closest to going insane
// kept being the ones picked.
//
// Sara is faction 35 and can never own an enemy list, so she cannot be the fixture caster.
// A Laughing Skull is the stand-in: faction 14, NullCreatureAI, no threat list, and the
// Lunatic Gaze test above already proves its spells select the bot as an enemy. Its own
// gaze is stripped first, otherwise the 2 sanity a second it drains drowns the oracle.
func TestAC_27590_PsychosisSkipsLowSanity(t *testing.T) {
meta.Begin(t, meta.TestMeta{
Tags: []string{"med", "instances", "issue"},
Runtime: "med",
Issue: 27590,
Category: "instances/northrend/ulduar",
})
const (
npcLaughingSkull = uint32(33990)
spellSanity = uint32(63050)
spellSanityWell = uint32(64169)
spellLunaticGaze = uint32(64167)
spellPsychosis = uint32(63795)
spellMalady = uint32(63830)
// Icecrown illusion chamber floor, the pad the Lunatic Gaze test spawns on.
padX, padY, padZ = float32(1930.0), float32(-120.0), float32(240.07)
// Threshold from the 2009-07-02 hotfix: at or below it neither spell may pick the player.
sanityFloor = 40
// Where the walk down starts. A Sanity Well tops up in steps of 20 from the single
// stack `.aura` creates, so 1 -> 21 -> 41 -> 61 lands here and Psychosis then steps
// 61 -> 52 -> 43 -> 34, straddling the threshold without overshooting it far.
sanityStart = 60
// 63795 is a 2.9s cast and target selection only runs once it completes. A cast that
// finds nobody expires quietly, so the no-drain half has to burn the whole window.
castWindow = 8 * time.Second
// Well ticks every 2s; this only has to outlast the three steps up to sanityStart.
rampWindow = 20 * time.Second
// Enough steps to walk sanityStart down past the threshold at 9 a hit, with room to spare.
maxPsychosisCasts = 20
)
bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{
Prefix: "YoggPs", Race: e2eharness.RaceHuman, Level: 80,
})
// Stay GM through the raid enter (.go xyz onto 603 is ignored after .gm off).
bot.Teleport(t, padX, padY, padZ, e2eharness.MapUlduar)
if _, _, _, m := bot.Pos(); m != e2eharness.MapUlduar {
e2eharness.Preconditionf(t, "not in Ulduar after brain room tele map=%d", m)
}
skull := bot.Spawn(t, npcLaughingSkull, 30*time.Second)
// GM casts and aura edits both act on the current selection, so every one of them says
// out loud which unit it means.
selectUnit := func(label string, guid uint64) {
t.Helper()
if err := bot.World.SetTarget(guid); err != nil {
e2eharness.Preconditionf(t, "select %s 0x%X: %v", label, guid, err)
}
}
selectSkull := func() { t.Helper(); selectUnit("skull", skull) }
selectSelf := func() { t.Helper(); selectUnit("self", bot.World.CharGUID()) }
selectSkull()
bot.GM(t, fmt.Sprintf(".unaura %d", spellLunaticGaze))
// Drop GM so the spells can select the bot; god mode absorbs the damage half of Psychosis.
bot.CombatReady(t)
bot.CheatGod(t)
// Sanity carries AURA_INTERRUPT_FLAG_CHANGE_MAP, so apply it after the tele. `.aura`
// creates it at one stack, which is already under the threshold, so a Sanity Well
// buff tops it up the same way Freya's wells do in the fight.
bot.ApplyAura(t, spellSanity)
bot.ApplyAura(t, spellSanityWell)
rampDeadline := time.Now().Add(rampWindow)
for bot.AuraStacks(spellSanity) < sanityStart && time.Now().Before(rampDeadline) {
time.Sleep(250 * time.Millisecond)
}
// CMSG_CANCEL_AURA does not take the well off, so strip it server side.
selectSelf()
bot.GM(t, fmt.Sprintf(".unaura %d", spellSanityWell))
start := bot.AuraStacks(spellSanity)
if start <= sanityFloor {
e2eharness.Preconditionf(t, "Sanity ramped to %d stacks, need more than %d to drive the threshold", start, sanityFloor)
}
// Every later delta is attributed to the spell under test, so nothing else may be moving
// sanity now: this catches both a skull that still gazes and a well that never came off.
time.Sleep(3 * time.Second)
if quiet := bot.AuraStacks(spellSanity); quiet != start {
e2eharness.Preconditionf(t, "sanity still moving on its own before the first cast (%d -> %d), skull 0x%X", start, quiet, skull)
}
// `.cast self` makes the *selected* unit cast on itself. Psychosis and Malady take their
// real targets from the area around the caster, which is the same selection path Sara
// drives in phase 2. Returns the stacks the bot lost, 0 if the cast never reached it.
castFromSkull := func(spellID uint32) int {
t.Helper()
before := bot.AuraStacks(spellSanity)
selectSkull()
bot.GM(t, fmt.Sprintf(".cast self %d", spellID))
deadline := time.Now().Add(castWindow)
for time.Now().Before(deadline) {
if now := bot.AuraStacks(spellSanity); now != before {
return before - now
}
time.Sleep(200 * time.Millisecond)
}
return 0
}
// Fixture check: at full sanity the spell must reach the bot at all. Without this a
// mis-aimed `.cast self` (it falls back to the caster's own player when nothing is
// selected) would read as a clean pass on both of the assertions below.
if lost := castFromSkull(spellMalady); lost <= 0 {
e2eharness.Preconditionf(t, "fixture dead: Malady of the Mind drained nothing at %d sanity", bot.AuraStacks(spellSanity))
}
lastValid := 0
for i := 0; i < maxPsychosisCasts; i++ {
before := bot.AuraStacks(spellSanity)
if before <= sanityFloor {
break
}
lost := castFromSkull(spellPsychosis)
if lost <= 0 {
e2eharness.ConfirmedBugf(t, 27590,
"Psychosis drained nothing at %d sanity, which is above the %d threshold", before, sanityFloor)
}
lastValid = before
t.Logf("Psychosis at %d sanity drained %d -> %d", before, lost, bot.AuraStacks(spellSanity))
}
atFloor := bot.AuraStacks(spellSanity)
if atFloor > sanityFloor {
e2eharness.Preconditionf(t, "never reached the threshold, stuck at %d sanity after %d casts", atFloor, maxPsychosisCasts)
}
if lost := castFromSkull(spellPsychosis); lost > 0 {
e2eharness.ConfirmedBugf(t, 27590,
"Psychosis drained %d sanity from a player at %d, at or below the %d threshold", lost, atFloor, sanityFloor)
}
atFloor = bot.AuraStacks(spellSanity)
if lost := castFromSkull(spellMalady); lost > 0 {
e2eharness.ConfirmedBugf(t, 27590,
"Malady of the Mind drained %d sanity from a player at %d, at or below the %d threshold", lost, atFloor, sanityFloor)
}
t.Logf("PASS low sanity targeting: both spells landed down to %d sanity and neither reached the player at %d", lastValid, atFloor)
}
@@ -2873,9 +2873,27 @@ class spell_yogg_saron_sanity_reduce : public SpellScript
}
}
// Psychosis and Malady of the Mind skip anyone at 40 Sanity or less, so that their random
// targeting evens out across the raid instead of finishing off the lowest players.
void FilterTargets(std::list<WorldObject*>& targets)
{
targets.remove_if([](WorldObject* target)
{
Unit* unit = target->ToUnit();
if (!unit)
return true;
Aura* sanity = unit->GetAura(SPELL_SANITY);
return !sanity || sanity->GetStackAmount() <= 40;
});
}
void Register() override
{
OnEffectHitTarget += SpellEffectFn(spell_yogg_saron_sanity_reduce::HandleScriptEffect, EFFECT_FIRST_FOUND, SPELL_EFFECT_SCRIPT_EFFECT);
if (m_scriptSpellId == SPELL_SARA_PSYCHOSIS_10 || m_scriptSpellId == SPELL_SARA_PSYCHOSIS_25 || m_scriptSpellId == SPELL_MALADY_OF_THE_MIND)
OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_yogg_saron_sanity_reduce::FilterTargets, EFFECT_ALL, TARGET_UNIT_SRC_AREA_ENEMY);
}
};