fix(Scripts/Ulduar): give Algalon a stasis period after Big Bang (#27630)

Co-authored-by: Shauren <[email protected]>
This commit is contained in:
Andrew
2026-09-13 16:59:43 -03:00
committed by GitHub
co-authored by Shauren
parent 46d4e0a39a
commit 92f5ce9a73
3 changed files with 281 additions and 5 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; 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 |
| 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; Algalon holds still for 3s once his Big Bang cast lands instead of resuming melee and Quantum Strike on the next tick | P2 | covered (`TestAC_27095_*`, `TestAC_27602_*`, `TestAC_27590_*`, `TestAC_27539_*`, `TestUlduar_BrightleafSunBeams*`); Kologarn Charge `blocked-harness` (bridge Z after Charge) | #26266 #27095 #27539 #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 |
---
@@ -10,6 +10,7 @@ import (
_ "github.com/go-sql-driver/mysql"
"github.com/azerothcore/AzerothGhost/client"
"github.com/azerothcore/AzerothGhost/e2e/e2eharness"
"github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta"
)
@@ -686,3 +687,254 @@ func TestAC_27590_PsychosisSkipsLowSanity(t *testing.T) {
t.Logf("PASS low sanity targeting: both spells landed down to %d sanity and neither reached the player at %d", lastValid, atFloor)
}
// Issue: https://github.com/azerothcore/azerothcore-wotlk/issues/27539
// PR: https://github.com/azerothcore/azerothcore-wotlk/pull/27630
// Big Bang is an 8s cast the raid survives by hiding inside a black hole. When it ends
// Algalon holds still for 3s before he picks the fight back up; AzerothCore resumed melee,
// Quantum Strike and the chase on the very next AI tick instead.
//
// The oracle is the bots' combined health, compared against a running peak because the pool
// regenerates while the fight runs: regeneration only raises the peak, so any decrease at all
// is Algalon landing something. Big Bang
// triggers 64445 on every player for exactly one second at the moment the cast lands, which
// is the protocol-visible marker the window is measured from. Reading the total rather than
// one bot's health keeps a tank swap mid-window from reading as a quiet boss.
//
// Two bots, because a solo one is not enough to hold the encounter up. Phase Punch's 5th
// stack applies 64417, a SPELL_AURA_PHASE that moves its target out of Algalon's phase for
// good (taking 64412 off afterwards does not remove it). With one player that leaves every
// threat reference offline via !CanSeeOrDetect, and Algalon sits flagged in combat unable to
// touch anyone. A second body is what a real raid's tank swap provides.
//
// Living Constellations are killed on sight because their Arcane Barrage is the one other
// thing that can reach a bot inside the window. Collapsing Stars are left alone: all four
// spawn points sit about 20y from where Algalon lands, well clear of a black hole's 6y reach.
func TestAC_27539_AlgalonBigBangStasis(t *testing.T) {
meta.Begin(t, meta.TestMeta{
Tags: []string{"long", "instances", "issue", "multi_bot"},
Runtime: "long",
Issue: 27539,
Category: "instances/northrend/ulduar",
})
const (
npcAlgalon = uint32(32871)
npcLivingConstellation = uint32(33052)
// Enough to drop a constellation in one hit.
constellationHit = uint32(500000)
goPlanetarium10 = uint32(194628)
goPlanetarium25 = uint32(194752)
itemKey10 = uint32(45797)
itemKey25 = uint32(45798)
spellPhasePunch = uint32(64412)
// The 5th Phase Punch stack: SPELL_AURA_PHASE, misc 16, the same phase a black hole
// puts a player in.
spellPhasePunchShift = uint32(64417)
spellRemovePhase = uint32(64445)
// Planetarium console, where Brann spawns and the summon roleplay starts.
consoleX, consoleY, consoleZ = float32(1646.2), float32(-174.7), float32(427.3)
// Off AlgalonLandPos, where he sets down. Standing on his exact coordinates leaves
// the facing check no direction and every swing returns SMSG_ATTACKSWING_BAD_FACING.
landX, landY, landZ = float32(1632.668), float32(-308.5), float32(417.321)
botHealth = 10000000
// Brann's walk plus Algalon's descent before EVENT_INTRO_FINISH drops SetImmuneToPC.
introWindow = 4 * time.Minute
// 90s fight timer plus the intro delay (26s on a first pull), with slack.
bigBangWindow = 3 * time.Minute
// Big Bang's damage and its 64445 marker come out of the same cast; give the health
// update from that cast time to arrive before the quiet window opens.
settle = 700 * time.Millisecond
// The script holds Algalon for 3s, but the server starts counting when it applies
// 64445 and this window starts when the client *sees* it, so the tail of a full 3s
// window runs past the moment he is allowed to move again. Stop short of the boundary
// by enough to cover that lag plus a world tick. A boss that never held still at all
// lands something around 1s, far inside what is still asserted.
stasisWindow = 2300 * time.Millisecond
// Quantum Strike repeats every 3-4.5s and melee is faster, so a boss that resumed
// lands something well inside this.
resumeWindow = 10 * time.Second
sampleEvery = 100 * time.Millisecond
// Throttle for the sweep that keeps the bots targetable and the field clear.
sweepEvery = 3 * time.Second
)
bots := e2eharness.NewScenario(t, e2eharness.ScenarioOpts{
Prefix: "AlgBB",
Bots: []e2eharness.BotSpec{
{Role: "tank", Race: e2eharness.RaceHuman, Class: e2eharness.ClassWarrior, Level: 80, LearnAllClass: true},
{Role: "spare", Race: e2eharness.RaceHuman, Class: e2eharness.ClassWarrior, Level: 80, LearnAllClass: true},
},
})
tank := e2eharness.ByRole(t, bots, "tank")
spare := e2eharness.ByRole(t, bots, "spare")
// Stay GM through the raid enter (.go xyz onto 603 is ignored after .gm off).
for _, b := range bots {
b.Teleport(t, consoleX, consoleY, consoleZ, e2eharness.MapUlduar)
if _, _, _, m := b.Pos(); m != e2eharness.MapUlduar {
e2eharness.Preconditionf(t, "%s not in Ulduar after console pad tele map=%d", b.Name, m)
}
}
// The console is a locked goober with one key entry per raid size, and which one spawned
// depends on the difficulty this instance came up at. Carry both.
tank.AddItem(t, itemKey10, 1)
tank.AddItem(t, itemKey25, 1)
console := e2eharness.TryNearbyGameObjectByEntry(t, tank.World, goPlanetarium10, 15*time.Second)
if console == 0 {
console = e2eharness.TryNearbyGameObjectByEntry(t, tank.World, goPlanetarium25, 15*time.Second)
}
if console == 0 {
e2eharness.Preconditionf(t, "no Celestial Planetarium Access console (%d/%d) at the pad", goPlanetarium10, goPlanetarium25)
}
tank.GameObjectUse(t, console)
// Brann walks the intro from the console end of the room; wait it out on the platform
// Algalon lands on. Instance grids are always loaded, so the roleplay runs regardless.
tank.Teleport(t, landX, landY, landZ, e2eharness.MapUlduar)
spare.Teleport(t, landX+4, landY-2, landZ, e2eharness.MapUlduar)
algalon := tank.WaitUnit(t, npcAlgalon, introWindow)
t.Logf("Algalon summoned guid=0x%X", algalon)
// Pulling him mid-roleplay is not the same fight: JustEngagedWith calls events.Reset(),
// which drops the pending EVENT_INTRO_FINISH that clears SetImmuneToPC, and he then never
// reaches EVENT_INTRO_TIMER_DONE to turn REACT_AGGRESSIVE. Wait for the flag to clear.
immune := func() bool {
o := tank.World.GetObject(algalon)
return o == nil || o.Value(client.UnitFieldFlags)&client.UnitFlagImmuneToPC != 0
}
introDeadline := time.Now().Add(introWindow)
for immune() && time.Now().Before(introDeadline) {
time.Sleep(500 * time.Millisecond)
}
if immune() {
e2eharness.Preconditionf(t, "Algalon 0x%X still UNIT_FLAG_IMMUNE_TO_PC after %s; intro roleplay never finished", algalon, introWindow)
}
t.Logf("intro roleplay done, Algalon attackable")
// Drop GM so he fights back, then make both bots able to sit through a Big Bang. Note the
// bare CombatReady/CombatReadyFull helpers turn `.cheat god` on, which would leave the
// bots invulnerable and every quiet window vacuous: this oracle needs real damage.
for _, b := range bots {
e2eharness.CombatReady(t, b.World, e2eharness.CombatReadyOpts{})
if err := b.World.SetTarget(b.World.CharGUID()); err != nil {
e2eharness.Preconditionf(t, "%s select self: %v", b.Name, err)
}
b.GM(t, fmt.Sprintf(".modify hp %d", botHealth))
}
tank.Engage(t, algalon, 60*time.Second)
t.Logf("engaged Algalon, waiting for Big Bang")
totalHP := func() uint32 {
var sum uint32
for _, b := range bots {
hp, _ := b.UnitHP(b.World.CharGUID())
sum += hp
}
return sum
}
sweep := func() bool {
// Stripped every pass rather than on a stack threshold: a missed read is
// unrecoverable, because once the 5th stack lands 64417 stays on and taking 64412 off
// does not remove it. This is the tank swap a real raid does, and Phase Punch only
// comes around every 15.5s.
for _, b := range bots {
if err := b.World.SetTarget(b.World.CharGUID()); err == nil {
b.GM(t, fmt.Sprintf(".unaura %d", spellPhasePunch))
b.GM(t, fmt.Sprintf(".unaura %d", spellPhasePunchShift))
}
}
if guids := e2eharness.LivingByEntries(tank.World, 300, npcLivingConstellation); len(guids) > 0 {
tank.DamageKill(t, guids, constellationHit, 10*time.Second)
t.Logf("cleared %d Living Constellation(s)", len(guids))
}
bossHP, _ := tank.UnitHP(algalon)
t.Logf("sweep: total hp=%d boss hp=%d bossTarget=0x%X inCombat=%v",
totalHP(), bossHP, tank.UnitTarget(algalon), tank.UnitInCombat(algalon))
tank.Attack(t, algalon)
return tank.UnitInCombat(algalon)
}
startHP := totalHP()
var bigBangAt time.Time
lastSweep := time.Time{}
waitDeadline := time.Now().Add(bigBangWindow)
for time.Now().Before(waitDeadline) {
if tank.HasAura(spellRemovePhase) || spare.HasAura(spellRemovePhase) {
bigBangAt = time.Now()
break
}
if time.Since(lastSweep) >= sweepEvery {
if !sweep() {
e2eharness.Preconditionf(t, "Algalon left combat before Big Bang (total hp %d -> %d)", startHP, totalHP())
}
lastSweep = time.Now()
}
time.Sleep(sampleEvery)
}
if bigBangAt.IsZero() {
e2eharness.Preconditionf(t, "no Big Bang (%d) within %s of the pull", spellRemovePhase, bigBangWindow)
}
// Oracle liveness: if nothing has been hitting the bots up to here, a quiet window proves
// nothing at all.
preHP := totalHP()
if preHP >= startHP {
e2eharness.Preconditionf(t, "bots took no damage before Big Bang (total %d -> %d), the quiet window would be vacuous", startHP, preHP)
}
t.Logf("Big Bang landed, total hp %d -> %d", startHP, preHP)
// The bots regenerate while this runs, so the total is not monotonic and a plain
// "below the value at window start" check would let regeneration mask a hit. Compare
// against a running peak instead: regeneration only raises it, and any decrease at all
// is Algalon landing something.
time.Sleep(settle)
quietHP := totalHP()
peakHP := quietHP
for time.Since(bigBangAt) < stasisWindow {
hp := totalHP()
if hp < peakHP {
e2eharness.ConfirmedBugf(t, 27539,
"Algalon acted %s after Big Bang, inside the 3s stasis: total hp %d -> %d",
time.Since(bigBangAt).Round(time.Millisecond), peakHP, hp)
}
if hp > peakHP {
peakHP = hp
}
time.Sleep(sampleEvery)
}
// The window must be quiet because he is holding still, not because he evaded or the bots
// stopped being targets: he has to come back swinging right after it.
resumed := false
resumeDeadline := time.Now().Add(resumeWindow)
for time.Now().Before(resumeDeadline) {
hp := totalHP()
if hp < peakHP {
resumed = true
break
}
if hp > peakHP {
peakHP = hp
}
time.Sleep(sampleEvery)
}
if !resumed {
e2eharness.Preconditionf(t,
"Algalon never resumed within %s of the stasis ending (total hp still %d, in combat=%v) — the quiet window proves nothing",
resumeWindow, quietHP, tank.UnitInCombat(algalon))
}
t.Logf("PASS Big Bang stasis: no damage for %s after the cast, then Algalon resumed", stasisWindow)
}
@@ -171,6 +171,11 @@ enum EncounterPhases
PHASE_MASK_NO_CAST_CHECK = 1 << (PHASE_ROLE_PLAY - 1),
};
enum EventGroups
{
GROUP_COMBAT = 1,
};
enum Texts
{
SAY_BRANN_ALGALON_INTRO_1 = 0,
@@ -395,6 +400,7 @@ struct boss_algalon_the_observer : public ScriptedAI
events.Reset();
summons.DespawnAll();
me->SetReactState(REACT_PASSIVE);
me->SetCombatMovement(true);
me->SetImmuneToPC(false);
me->SetSheath(SHEATH_STATE_UNARMED);
me->SetFaction(190);
@@ -477,6 +483,7 @@ struct boss_algalon_the_observer : public ScriptedAI
case ACTION_ASCEND:
summons.DespawnAll();
events.SetPhase(PHASE_BIG_BANG);
events.CancelEvent(EVENT_RESUME_UPDATING);
events.ScheduleEvent(EVENT_ASCEND_TO_THE_HEAVENS, 1500ms);
break;
case ACTION_FEEDS_ON_TEARS_FAILED:
@@ -522,10 +529,10 @@ struct boss_algalon_the_observer : public ScriptedAI
introDelay = 8500ms;
events.ScheduleEvent(EVENT_INTRO_TIMER_DONE, introDelay);
events.ScheduleEvent(EVENT_QUANTUM_STRIKE, 3500ms + introDelay);
events.ScheduleEvent(EVENT_PHASE_PUNCH, 15500ms + introDelay);
events.ScheduleEvent(EVENT_QUANTUM_STRIKE, 3500ms + introDelay, GROUP_COMBAT);
events.ScheduleEvent(EVENT_PHASE_PUNCH, 15500ms + introDelay, GROUP_COMBAT);
events.ScheduleEvent(EVENT_SUMMON_COLLAPSING_STAR, 16500ms + introDelay);
events.ScheduleEvent(EVENT_COSMIC_SMASH, 26s + introDelay);
events.ScheduleEvent(EVENT_COSMIC_SMASH, 26s + introDelay, GROUP_COMBAT);
events.ScheduleEvent(EVENT_ACTIVATE_LIVING_CONSTELLATION, 60s + introDelay);
events.ScheduleEvent(EVENT_BIG_BANG, 90s + introDelay);
events.ScheduleEvent(EVENT_ASCEND_TO_THE_HEAVENS, 360s + introDelay);
@@ -745,13 +752,29 @@ struct boss_algalon_the_observer : public ScriptedAI
summons.DoAction(ACTION_BIG_BANG, pred);
me->CastSpell((Unit*)nullptr, SPELL_BIG_BANG, false);
// 8s cast, then Algalon holds still for 3s before resuming
events.SetPhase(PHASE_BIG_BANG);
events.ScheduleEvent(EVENT_RESUME_UPDATING, 11s);
events.DelayEventsToMax(11s, GROUP_COMBAT);
events.Repeat(90s + 500ms);
me->SetCombatMovement(false);
me->GetMotionMaster()->Clear(false);
me->GetMotionMaster()->MoveIdle();
break;
}
case EVENT_RESUME_UPDATING:
events.SetPhase(PHASE_NORMAL);
me->SetCombatMovement(true);
me->ResumeChasingVictim();
break;
case EVENT_ASCEND_TO_THE_HEAVENS:
Talk(SAY_ALGALON_ASCEND);
me->CastSpell((Unit*)nullptr, SPELL_ASCEND_TO_THE_HEAVENS, false);
events.ScheduleEvent(EVENT_EVADE, 2500ms);
events.CancelEvent(EVENT_RESUME_UPDATING);
me->SetCombatMovement(true);
break;
case EVENT_EVADE:
events.Reset();
@@ -849,7 +872,8 @@ struct boss_algalon_the_observer : public ScriptedAI
break;
}
DoMeleeAttackIfReady();
if (!(events.GetPhaseMask() & PHASE_MASK_NO_UPDATE))
DoMeleeAttackIfReady();
}
};