fix(Scripts/Ulduar): let the hard-reset respawn bring Algalon back after a wipe (#27807)

This commit is contained in:
Andrew
2026-09-26 11:32:56 -03:00
committed by GitHub
parent 5727348eb5
commit 93ab9ad63e
5 changed files with 341 additions and 67 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; a wave of Elder Brightleaf's Unstable Sun Beams stays capped however many players stand in range; 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; a Writhing Lasher stays up past 62947's own 10s summon duration, because the despawn type its AI sets in `IsSummonedBy` is no longer overwritten on the way out of the summon call; a wipe to Thorim's melee with a death-persistent damage shield on the last victim spawns no Cache of Storms while his hard-reset despawn runs, and a lethal hit on the respawned Thorim still yields, runs the outro to his teleport-out without an evade and leaves the saved encounter DONE; an Ancient Water Spirit whose Tidal Wave is kicked casts no damage-and-knockback follow-up; one Elder left alive spawns the one-Elder Freya's Gift, which is the chest whose emblem count the fix corrects; Ignis dying kills every Iron Construct including the dormant ones, and his Kill All Constructs instakill spares the player and every other creature in range | P2 | covered (`TestAC_27095_*`, `TestAC_27602_*`, `TestAC_27590_*`, `TestAC_27539_*`, `TestAC_27455_*`, `TestUlduar_BrightleafSunBeamsCappedPerWave`, `TestUlduar_FreyaWardLasherOutlivesSummonDuration`, `TestUlduar_ThorimEvadeDespawnDoesNotYield`, `TestUlduar_FreyaGiftMatchesElderCount`, `TestAC_27736_IgnisKillsAllConstructs`); Kologarn Charge `blocked-harness` (bridge Z after Charge); Freya's Gift emblem counts `blocked-harness` (a chest's loot opens only through SPELL_EFFECT_OPEN_LOCK, and CMSG_CAST_SPELL carries no TARGET_FLAG_GAMEOBJECT); Ignis constructs on 25-man `blocked-harness` (no way to set a bot's raid difficulty: CMSG_SET_RAID_DIFFICULTY is unexported and a `characters`.`instance_mode_mask` write is overwritten by the save on Relog's logout) | #26266 #27095 #27455 #27539 #27590 #27602 #27736 |
| 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; a wave of Elder Brightleaf's Unstable Sun Beams stays capped however many players stand in range; 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; a Writhing Lasher stays up past 62947's own 10s summon duration, because the despawn type its AI sets in `IsSummonedBy` is no longer overwritten on the way out of the summon call; a wipe to Thorim's melee with a death-persistent damage shield on the last victim spawns no Cache of Storms while his hard-reset despawn runs, and a lethal hit on the respawned Thorim still yields, runs the outro to his teleport-out without an evade and leaves the saved encounter DONE; an Ancient Water Spirit whose Tidal Wave is kicked casts no damage-and-knockback follow-up; one Elder left alive spawns the one-Elder Freya's Gift, which is the chest whose emblem count the fix corrects; Ignis dying kills every Iron Construct including the dormant ones, and his Kill All Constructs instakill spares the player and every other creature in range; after a wipe exactly one Algalon comes back through his hard-reset respawn, arriving from the sky flagged immune before he lands, even when a player re-enters the raid inside the 20s respawn window, and it stays one past the 30s reload fallback | P2 | covered (`TestAC_27095_*`, `TestAC_27602_*`, `TestAC_27590_*`, `TestAC_27539_*`, `TestAC_27455_*`, `TestUlduar_BrightleafSunBeamsCappedPerWave`, `TestUlduar_FreyaWardLasherOutlivesSummonDuration`, `TestUlduar_ThorimEvadeDespawnDoesNotYield`, `TestUlduar_FreyaGiftMatchesElderCount`, `TestAC_27736_IgnisKillsAllConstructs`, `TestUlduar_AlgalonWipeRespawnsOnce`); Kologarn Charge `blocked-harness` (bridge Z after Charge); Freya's Gift emblem counts `blocked-harness` (a chest's loot opens only through SPELL_EFFECT_OPEN_LOCK, and CMSG_CAST_SPELL carries no TARGET_FLAG_GAMEOBJECT); Ignis constructs on 25-man `blocked-harness` (no way to set a bot's raid difficulty: CMSG_SET_RAID_DIFFICULTY is unexported and a `characters`.`instance_mode_mask` write is overwritten by the save on Relog's logout) | #26266 #27095 #27455 #27539 #27590 #27602 #27736 |
| professions/trainers | every Engineering trainer group offers a visitor exactly the twelve TBC master-tier recipes its class can learn — and none of the others, so a broken class filter fails too. Eleven are class-restricted goggles: four classes cover all twelve across the eight groups, two characters per class cover the Horde/Alliance and Aldor/Scryer interaction splits, and the remaining five classes pin the armour-type sharing at Technician Mihila. Why a hidden recipe is hidden is pinned too: either the class cannot wear the goggle at all — the refusal read from SMSG_INVENTORY_CHANGE_FAILURE, which must be EQUIP_ERR_NO_REQUIRED_PROFICIENCY — or the goggle is another class's within the same armour type, which is why armour proficiency would be the wrong filter to gate on | P2 | covered (`TestAC_27146_*`) | #27146 |
| world/dalaran | Dalaran's faction guards eject a hostile player from the Silver Enclave and Sunreaver's Sanctuary, and from nowhere else — public mailboxes, the street, the sewers and the neutral inn are left alone. Both directions are asserted: only checking public ground would pass a build where the guards never fire. One case parks a pet inside a quarter with its owner outside, since every other case is a lone player and would not notice the guard reading the owner's position instead of the trespasser's | P2 | covered (`TestAC_4467_*`) | #4467 |
| 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 |
@@ -0,0 +1,263 @@
//go:build e2e
package ulduar_test
import (
"testing"
"time"
_ "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"
)
// Issue: https://github.com/chromiecraft/chromiecraft/issues/10228
// Earlier upstream report of the same symptom: https://github.com/azerothcore/azerothcore-wotlk/issues/26759
//
// Algalon is a temp summon with CREATURE_FLAG_EXTRA_HARD_RESET: a wipe despawns him and the map
// re-summons him 20s later (Map::ScheduleCreatureRespawn). The instance script used to keep a
// resummon of its own and summon him outright from OnPlayerEnter, so a player who ran back into
// Ulduar inside that window found a second Algalon standing in the first one's spot.
//
// Oracle: after a wipe, with the player re-entering the raid inside the respawn window, exactly
// one Algalon comes back, and it stays one past the 30s point where the reload fallback fires.
// The one that comes back replays the arrival: it summons the Azeroth planet, drops the immunity
// flag and ends up resting where the first arrival did.
//
// One bot is enough: the wipe is the bot's own death after Algalon has turned aggressive, not a
// fight that has to be held up.
func TestUlduar_AlgalonWipeRespawnsOnce(t *testing.T) {
meta.Begin(t, meta.TestMeta{
Tags: []string{"long", "instances"},
Runtime: "long",
Category: "instances/northrend/ulduar",
})
const (
npcAlgalon = uint32(32871)
// The planet Algalon summons during his arrival roleplay.
npcAzeroth = uint32(34246)
goPlanetarium10 = uint32(194628)
goPlanetarium25 = uint32(194752)
itemKey10 = uint32(45797)
itemKey25 = uint32(45798)
// 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.
landX, landY, landZ = float32(1632.668), float32(-308.5), float32(417.321)
// Brann's walk plus Algalon's descent before EVENT_INTRO_FINISH drops SetImmuneToPC.
introWindow = 4 * time.Minute
// 26s intro delay on a first pull before EVENT_INTRO_TIMER_DONE clears NOT_SELECTABLE,
// the point from which UpdateVictim runs and a wipe can evade him.
pullWindow = 90 * time.Second
evadeWindow = 30 * time.Second
// DespawnOnEvade's respawn delay is 20s; the re-entry has to land inside it.
respawnDelay = 20 * time.Second
respawnWindow = 45 * time.Second
// OnPlayerEnter's reload fallback fires 30s after the re-entry; watch past it.
fallbackWindow = 45 * time.Second
// EVENT_INTRO_SUMMON fires 7s into the arrival.
azerothWindow = 20 * time.Second
// The re-arrival descends at 3s and is done at 5s; a create after that is plain.
settledAfter = 15 * time.Second
sampleEvery = 200 * time.Millisecond
)
bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{
Prefix: "AlgRsp", Race: e2eharness.RaceHuman,
Class: e2eharness.ClassWarrior, Level: 80, LearnAllClass: true,
})
// Stay GM through the raid enter (.go xyz onto 603 is ignored after .gm off).
bot.Teleport(t, consoleX, consoleY, consoleZ, e2eharness.MapUlduar)
if _, _, _, m := bot.Pos(); m != e2eharness.MapUlduar {
e2eharness.Preconditionf(t, "bot not in Ulduar after console pad tele map=%d", 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.
bot.AddItem(t, itemKey10, 1)
bot.AddItem(t, itemKey25, 1)
console := e2eharness.TryNearbyGameObjectByEntry(t, bot.World, goPlanetarium10, 15*time.Second)
if console == 0 {
console = e2eharness.TryNearbyGameObjectByEntry(t, bot.World, goPlanetarium25, 15*time.Second)
}
if console == 0 {
e2eharness.Preconditionf(t, "no Celestial Planetarium Access console (%d/%d) at the pad", goPlanetarium10, goPlanetarium25)
}
bot.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.
bot.Teleport(t, landX, landY, landZ, e2eharness.MapUlduar)
first := bot.WaitUnit(t, npcAlgalon, introWindow)
t.Logf("Algalon summoned guid=0x%X", first)
flags := func(guid uint64) (uint32, bool) {
o := bot.World.GetObject(guid)
if o == nil {
return 0, false
}
return o.Value(client.UnitFieldFlags), true
}
// Returns once the flag is clear; a creature that leaves the cache is reported as a
// precondition, since a stale flag and a despawn would otherwise read the same.
waitFlagClear := func(guid uint64, flag uint32, timeout time.Duration, what string) bool {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
f, ok := flags(guid)
if !ok {
e2eharness.Preconditionf(t, "Algalon 0x%X vanished while waiting for %s", guid, what)
}
if f&flag == 0 {
return true
}
time.Sleep(sampleEvery)
}
return false
}
// Pulling him mid-roleplay is not the same fight: JustEngagedWith calls events.Reset(),
// which drops the pending EVENT_INTRO_FINISH that clears SetImmuneToPC. Wait for the flag.
if !waitFlagClear(first, client.UnitFlagImmuneToPC, introWindow, "the arrival roleplay to finish") {
e2eharness.Preconditionf(t, "Algalon 0x%X still UNIT_FLAG_IMMUNE_TO_PC after %s; intro roleplay never finished", first, introWindow)
}
firstRest := bot.World.GetObject(first)
if firstRest == nil {
e2eharness.Preconditionf(t, "Algalon 0x%X left the cache right after his arrival", first)
}
restX, restY, restZ := firstRest.PosX, firstRest.PosY, firstRest.PosZ
t.Logf("intro roleplay done, Algalon attackable, resting at (%.1f, %.1f, %.1f)", restX, restY, restZ)
// Drop GM so he fights back, with god on: an ungeared bot dies to his first swings and
// he would evade before the hand-over below is observed. DieMust drops god for the wipe.
e2eharness.CombatReady(t, bot.World, e2eharness.CombatReadyOpts{God: true})
bot.Engage(t, first, 60*time.Second)
if !waitFlagClear(first, client.UnitFlagNotSelectable, pullWindow, "the engage roleplay to hand over to combat") {
e2eharness.Preconditionf(t, "Algalon 0x%X still NOT_SELECTABLE %s after the pull; the engage roleplay never handed over to combat", first, pullWindow)
}
t.Logf("Algalon in combat and aggressive, wiping")
// The wipe: with nobody alive within 120y his evade fails the encounter, and the hard-reset
// despawn removes this creature outright.
bot.DieMust(t, 25*time.Second)
evadeDeadline := time.Now().Add(evadeWindow)
for bot.World.GetObject(first) != nil && time.Now().Before(evadeDeadline) {
time.Sleep(sampleEvery)
}
if bot.World.GetObject(first) != nil {
e2eharness.Preconditionf(t, "Algalon 0x%X still present %s after the wipe: no evade despawn", first, evadeWindow)
}
evadeAt := time.Now()
t.Logf("Algalon 0x%X despawned on the wipe at %s", first, evadeAt.Format(time.StampMilli))
// Run back inside the respawn window: leave the raid and come back through the map
// transfer, which is what fires OnPlayerEnter. GM mode again for the .go onto 603.
bot.GM(t, ".revive")
bot.WaitAlive(t, 10*time.Second)
bot.GM(t, ".gm on")
bot.FlushWorld(t)
bot.TeleNamed(t, "dalaran")
if _, _, _, m := bot.Pos(); m == e2eharness.MapUlduar {
e2eharness.Preconditionf(t, "bot still in Ulduar after the tele out")
}
bot.Teleport(t, landX, landY, landZ, e2eharness.MapUlduar)
if _, _, _, m := bot.Pos(); m != e2eharness.MapUlduar {
e2eharness.Preconditionf(t, "bot not back in Ulduar map=%d", m)
}
reenterAt := time.Now()
if reenterAt.Sub(evadeAt) >= respawnDelay {
e2eharness.Preconditionf(t, "re-entered the raid %s after the evade, outside the %s respawn window the race needs",
reenterAt.Sub(evadeAt).Round(time.Millisecond), respawnDelay)
}
t.Logf("re-entered Ulduar %s after the evade", reenterAt.Sub(evadeAt).Round(time.Millisecond))
// The respawn's create packet carries his flight spline, and the harness reads no values out
// of such a create (entry and health come back 0) and later loses track of the object, so the
// living-unit helpers cannot see him until a plain create arrives. Until the relog below, count
// by the entry packed in the GUID and only ever fail on a second one: a fallback summon is a
// plain create and always shows up.
algalons := func() []uint64 {
var out []uint64
for _, u := range bot.NearbyUnits(300) {
if uint32((u.GUID>>24)&0xFFFFFF) == npcAlgalon {
out = append(out, u.GUID)
}
}
return out
}
failOnSecond := func(seen []uint64, since time.Time, what string) {
if len(seen) > 1 {
e2eharness.Assertf(t, "%d Algalons %s after %s (guids %v): the re-entry summoned one next to the respawn",
len(seen), time.Since(since).Round(time.Millisecond), what, seen)
}
}
// Exactly one Algalon comes back.
var respawn uint64
respawnDeadline := evadeAt.Add(respawnWindow)
for time.Now().Before(respawnDeadline) {
seen := algalons()
failOnSecond(seen, evadeAt, "the wipe")
if len(seen) == 1 {
respawn = seen[0]
break
}
time.Sleep(sampleEvery)
}
if respawn == 0 {
e2eharness.Assertf(t, "no Algalon came back within %s of the wipe", respawnWindow)
}
if respawn == first {
e2eharness.Assertf(t, "Algalon 0x%X came back with the guid of the despawned creature", respawn)
}
backAt := time.Now()
t.Logf("Algalon back as 0x%X %s after the wipe", respawn, backAt.Sub(evadeAt).Round(time.Millisecond))
// The arrival roleplay summons the planet 7s in; a bare summon at his home position never does.
azeroth := bot.WaitUnit(t, npcAzeroth, azerothWindow)
t.Logf("arrival replayed: Azeroth 0x%X summoned", azeroth)
// Keep watching for a second one until his last arrival movement (the descent at 10s) is over,
// then relog for a plain, readable create of him.
for time.Now().Before(backAt.Add(settledAfter)) {
failOnSecond(algalons(), reenterAt, "the re-entry")
time.Sleep(sampleEvery)
}
bot.Relog(t)
if landed := bot.WaitUnit(t, npcAlgalon, 30*time.Second); landed != respawn {
e2eharness.Assertf(t, "Algalon after the relog is 0x%X, not the respawn 0x%X", landed, respawn)
}
// Still exactly one past the fallback the re-entry scheduled.
holdDeadline := reenterAt.Add(fallbackWindow)
for time.Now().Before(holdDeadline) {
living := e2eharness.LivingByEntries(bot.World, 300, npcAlgalon)
if seen := algalons(); len(seen) != 1 || len(living) != 1 {
e2eharness.Assertf(t, "%d Algalons (%d living) %s after the re-entry (guids %v), want exactly the respawn 0x%X",
len(seen), len(living), time.Since(reenterAt).Round(time.Millisecond), seen, respawn)
}
time.Sleep(sampleEvery)
}
t.Logf("still one Algalon %s after the re-entry", fallbackWindow)
// The arrival finishes: he becomes attackable and rests where the first arrival did.
if !waitFlagClear(respawn, client.UnitFlagImmuneToPC, introWindow, "the replayed arrival to finish") {
e2eharness.Assertf(t, "respawned Algalon 0x%X never dropped UNIT_FLAG_IMMUNE_TO_PC within %s", respawn, introWindow)
}
o := bot.World.GetObject(respawn)
if o == nil {
e2eharness.Assertf(t, "respawned Algalon 0x%X gone after finishing the arrival", respawn)
}
if d := e2eharness.Distance3D(o.PosX, o.PosY, o.PosZ, restX, restY, restZ); d > 3 {
e2eharness.Assertf(t, "respawned Algalon 0x%X finished the arrival at (%.1f, %.1f, %.1f), %.1fy from where the first arrival ended (%.1f, %.1f, %.1f)",
respawn, o.PosX, o.PosY, o.PosZ, d, restX, restY, restZ)
}
t.Logf("PASS one Algalon (0x%X) back after the wipe, arrival replayed and resting at (%.1f, %.1f, %.1f)", respawn, o.PosX, o.PosY, o.PosZ)
}
@@ -363,7 +363,7 @@ struct boss_algalon_the_observer : public ScriptedAI
}
}
void EnterEvadeMode(EvadeReason why) override
void EnterEvadeMode(EvadeReason /*why*/) override
{
if (_fightWon)
return;
@@ -373,23 +373,23 @@ struct boss_algalon_the_observer : public ScriptedAI
me->SetInCombatWithZone();
return;
}
else if (events.GetPhaseMask() & PHASE_NORMAL)
// A wipe ends with Ascend to the Heavens; EVENT_EVADE then fails the encounter
DoAction(ACTION_ASCEND);
}
// Algalon has CREATURE_FLAG_EXTRA_HARD_RESET: the base evade despawns him and the map re-summons him
// 20s later at his home position, so point that at the sky for the arrival the instance script replays.
void FailEncounter(EvadeReason why)
{
if (_instance)
{
DoAction(ACTION_ASCEND);
return;
_instance->SetBossState(BOSS_ALGALON, FAIL);
_instance->StorePersistentData(PERSISTENT_DATA_ALGALON_FIRST_PULL, 1);
}
if (_instance)
_instance->SetBossState(BOSS_ALGALON, FAIL);
if (!_EnterEvadeMode(why))
return;
me->GetMotionMaster()->MoveTargetedHome();
Reset();
sScriptMgr->OnUnitEnterEvadeMode(me, why);
me->SetHomePosition(AlgalonSummonPos);
ScriptedAI::EnterEvadeMode(why);
}
void Reset() override
@@ -408,16 +408,7 @@ struct boss_algalon_the_observer : public ScriptedAI
_phaseTwo = false;
_heraldOfTheTitans = true;
if (_instance)
{
if (_instance->GetBossState(BOSS_ALGALON) == FAIL)
{
_firstPull = false;
_instance->StorePersistentData(PERSISTENT_DATA_ALGALON_FIRST_PULL, 1);
_instance->SetData(DATA_RESUMMON_ALGALON, 0);
me->DespawnOrUnsummon(1ms);
}
_instance->SetBossState(BOSS_ALGALON, NOT_STARTED);
}
}
void KilledUnit(Unit* victim) override
@@ -443,13 +434,25 @@ struct boss_algalon_the_observer : public ScriptedAI
me->SetHomePosition(AlgalonLandPos);
events.Reset();
events.SetPhase(PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_1, 5s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_CHANNEL, 6s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_SUMMON, 7s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_DESCEND, 10s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_2, 15s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_3, 23s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_FINISH, 36s, 0, PHASE_ROLE_PLAY);
if (_firstPull)
{
events.ScheduleEvent(EVENT_INTRO_1, 5s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_CHANNEL, 6s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_SUMMON, 7s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_DESCEND, 10s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_2, 15s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_3, 23s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_FINISH, 36s, 0, PHASE_ROLE_PLAY);
}
else
{
// The re-arrival after a wipe has no dialogue, so the visuals run back to back
events.ScheduleEvent(EVENT_INTRO_1, 1s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_CHANNEL, 1500ms, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_SUMMON, 2s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_DESCEND, 3s, 0, PHASE_ROLE_PLAY);
events.ScheduleEvent(EVENT_INTRO_FINISH, 5s, 0, PHASE_ROLE_PLAY);
}
break;
}
case ACTION_DESPAWN_ALGALON:
@@ -778,7 +781,7 @@ struct boss_algalon_the_observer : public ScriptedAI
break;
case EVENT_EVADE:
events.Reset();
ScriptedAI::EnterEvadeMode();
FailEncounter(EVADE_REASON_OTHER);
return;
case EVENT_OUTRO_START:
if (_instance)
@@ -256,7 +256,7 @@ public:
// Shared
EventMap _events;
bool _mimironTramUsed;
bool _algalonResummonPending;
bool _algalonArrivalPending;
void Initialize() override
{
@@ -281,7 +281,7 @@ public:
// Shared
_events.Reset();
_mimironTramUsed = false;
_algalonResummonPending = false;
_algalonArrivalPending = false;
}
void FillInitialWorldStates(WorldPackets::WorldState::InitWorldStates& packet) override
@@ -515,29 +515,13 @@ public:
ObservationRingKeeperEntry[i],
ObservationRingKeepersPos[i]);
uint32 algalonTimer =
GetPersistentData(PERSISTENT_DATA_ALGALON_TIMER);
if (!GetObjectGuid(BOSS_ALGALON) && !_algalonResummonPending && algalonTimer
&& (algalonTimer <= 60
|| algalonTimer == TIMER_ALGALON_TO_SUMMON))
{
TempSummon* algalon = instance->SummonCreature(NPC_ALGALON, AlgalonLandPos);
if (!algalon)
return;
if (algalonTimer <= 60)
{
_events.RescheduleEvent(EVENT_UPDATE_ALGALON_TIMER, 1min);
algalon->AI()->DoAction(ACTION_INIT_ALGALON);
}
else // if (algalonTimer == TIMER_ALGALON_TO_SUMMON)
{
StorePersistentData(
PERSISTENT_DATA_ALGALON_TIMER,
TIMER_ALGALON_SUMMONED);
algalon->SetImmuneToPC(false);
}
}
// Only covers an instance reload: after a wipe the map re-summons Algalon itself 20s after
// the evade, so wait long enough for that respawn to land first.
uint32 algalonTimer = GetPersistentData(PERSISTENT_DATA_ALGALON_TIMER);
if (!GetCreature(BOSS_ALGALON) && algalonTimer
&& (algalonTimer <= 60 || algalonTimer == TIMER_ALGALON_TO_SUMMON)
&& !_events.HasTimeUntilEvent(EVENT_RESUMMON_ALGALON))
_events.ScheduleEvent(EVENT_RESUMMON_ALGALON, 30s);
}
bool IsEncounterInProgress() const override
@@ -677,6 +661,11 @@ public:
case NPC_ALGALON:
if (!GetPersistentData(PERSISTENT_DATA_ALGALON_TIMER))
creature->DespawnOrUnsummon();
else if (_algalonArrivalPending)
{
_algalonArrivalPending = false;
creature->AI()->DoAction(ACTION_START_INTRO);
}
break;
// Gone for good once Flame Leviathan is defeated
case NPC_STEELFORGED_DEFENDER:
@@ -714,6 +703,13 @@ public:
}
}
void OnCreatureEvade(Creature* creature) override
{
// The map re-summons Algalon 20s after his hard-reset evade; the new one replays the arrival
if (creature->GetEntry() == NPC_ALGALON && GetBossState(BOSS_ALGALON) != DONE)
_algalonArrivalPending = true;
}
void OpenIfDone(uint32 encounter, GameObject* go, GOState state)
{
if (GetBossState(encounter) == DONE)
@@ -976,10 +972,6 @@ public:
StorePersistentData(PERSISTENT_DATA_ALGALON_TIMER, 60);
_events.RescheduleEvent(EVENT_UPDATE_ALGALON_TIMER, 1min);
return;
case DATA_RESUMMON_ALGALON:
_algalonResummonPending = true;
_events.RescheduleEvent(EVENT_RESUMMON_ALGALON, 2s);
return;
case DATA_ALGALON_SUMMON_STATE:
case DATA_ALGALON_DEFEATED:
DoUpdateWorldState(WORLD_STATE_ULDUAR_ALGALON_TIMER_ENABLED, 0);
@@ -1209,11 +1201,28 @@ public:
break;
}
case EVENT_RESUMMON_ALGALON:
_algalonResummonPending = false;
if (!GetCreature(BOSS_ALGALON))
if (Creature* algalon = instance->SummonCreature(NPC_ALGALON, AlgalonSummonPos))
algalon->AI()->DoAction(ACTION_START_INTRO);
{
uint32 algalonTimer = GetPersistentData(PERSISTENT_DATA_ALGALON_TIMER);
if (GetCreature(BOSS_ALGALON) || !algalonTimer
|| (algalonTimer > 60 && algalonTimer != TIMER_ALGALON_TO_SUMMON))
break;
TempSummon* algalon = instance->SummonCreature(NPC_ALGALON, AlgalonLandPos);
if (!algalon)
break;
if (algalonTimer <= 60)
{
_events.RescheduleEvent(EVENT_UPDATE_ALGALON_TIMER, 1min);
algalon->AI()->DoAction(ACTION_INIT_ALGALON);
}
else // TIMER_ALGALON_TO_SUMMON
{
StorePersistentData(PERSISTENT_DATA_ALGALON_TIMER, TIMER_ALGALON_SUMMONED);
algalon->SetImmuneToPC(false);
}
break;
}
}
}
@@ -111,7 +111,6 @@ enum UlduarData
DATA_UNIVERSE_GLOBE = 608,
DATA_ALGALON_TRAPDOOR = 609,
DATA_BRANN_BRONZEBEARD_ALG = 610,
DATA_RESUMMON_ALGALON = 611,
// Achievements
DATA_DWARFAGEDDON = 700,