From c0e6272af9a1a95aa0859a0cd95f7b70b273c9f4 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:28:07 -0300 Subject: [PATCH] fix(Scripts/Ulduar): make Yogg-Saron's Laughing Skulls respect line of sight (#27614) --- e2e/README.md | 2 +- .../northrend/ulduar/ulduar_e2e_test.go | 78 +++++++++++++++++++ .../Ulduar/Ulduar/boss_yoggsaron.cpp | 17 ++-- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/e2e/README.md b/e2e/README.md index 146fc01a59..70c32959d6 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -163,7 +163,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 | P2 | covered (`TestAC_27095_*`); Kologarn Charge `blocked-harness` (bridge Z after Charge) | #26266 #27095 | +| 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 | P2 | covered (`TestAC_27095_*`, `TestAC_27602_*`); Kologarn Charge `blocked-harness` (bridge Z after Charge) | #26266 #27095 #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 | --- diff --git a/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go b/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go index 48597240be..e44b8bcc24 100644 --- a/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go +++ b/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go @@ -277,3 +277,81 @@ func TestUlduar_MultiBotLoginNearBossPad(t *testing.T) { } t.Logf("PASS multi-bot Freya pad login n=%d map=%d", len(bots), m0) } + +// Issue: https://github.com/azerothcore/azerothcore-wotlk/issues/27602 +// PR: https://github.com/azerothcore/azerothcore-wotlk/pull/27614 +// A Laughing Skull's Lunatic Gaze (64168) must not reach a player behind the brain +// room's geometry. 64168 has no ignore-LoS attribute of its own; it used to inherit +// one from the aura that triggers it (64167), so the skulls drained sanity through +// walls. The clear-line half runs first and is the fixture check: without sanity +// loss there, the blocked half proves nothing. +func TestAC_27602_LaughingSkullGazeLoS(t *testing.T) { + meta.Begin(t, meta.TestMeta{ + Tags: []string{"med", "instances", "issue"}, + Runtime: "med", + Issue: 27602, + Category: "instances/northrend/ulduar", + }) + + const ( + npcLaughingSkull = uint32(33990) + spellSanity = uint32(63050) + + // Icecrown illusion chamber floor. The pair sits 18 yd either side of the + // skull: due west is open, due east is behind structure (verified in-world + // with a LoS-respecting player cast at 12, 18 and 24 yd). + skullX, skullY, skullZ = float32(1930.0), float32(-120.0), float32(240.07) + clearX, clearY = float32(1912.0), float32(-120.0) + blockedX, blockedY = float32(1948.0), float32(-120.0) + + // 64167 ticks every second for 2 sanity; 8s leaves margin for spawn settle. + gazeWindow = 8 * time.Second + ) + + bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{ + Prefix: "YoggLo", Race: e2eharness.RaceHuman, Level: 80, + }) + + // Stay GM through the raid enter (.go xyz onto 603 is ignored after .gm off). + bot.Teleport(t, skullX, skullY, skullZ, e2eharness.MapUlduar) + if _, _, _, m := bot.Pos(); m != e2eharness.MapUlduar { + e2eharness.Preconditionf(t, "not in Ulduar after brain room tele map=%d", m) + } + // The skulls are SummonCreature'd by the Brain AI, not by a spell, so a GM spawn + // is the fixture rather than a stand-in for a summon path. 64167 rides on + // creature_template_addon, so the spawn gazes on its own with no AI to drive. + skull := bot.Spawn(t, npcLaughingSkull, 30*time.Second) + + // Drop GM so the gaze can select the bot; god mode absorbs the 1749/s it deals. + bot.CombatReady(t) + bot.CheatGod(t) + + // Sanity carries AURA_INTERRUPT_FLAG_CHANGE_MAP, so apply it after the tele. + sanityLost := func(label string, px, py float32) int { + bot.Teleport(t, px, py, skullZ, e2eharness.MapUlduar) + bot.WaitUnitGUID(t, skull, 15*time.Second) // tele clears the object cache + bot.Face(t, skull) // 64168 only takes targets facing the caster + bot.ApplyAura(t, spellSanity) + before := bot.AuraStacks(spellSanity) + if before == 0 { + e2eharness.Preconditionf(t, "%s: Sanity 63050 did not apply", label) + } + time.Sleep(gazeWindow) + after := bot.AuraStacks(spellSanity) + bot.CancelAura(t, spellSanity) + t.Logf("%s pos=(%.1f,%.1f) skull=0x%X sanity %d -> %d", label, px, py, skull, before, after) + return before - after + } + + clearLoss := sanityLost("CLEAR", clearX, clearY) + if clearLoss <= 0 { + e2eharness.Preconditionf(t, "fixture dead: skull 0x%X drained no sanity with a clear line", skull) + } + blockedLoss := sanityLost("BLOCKED", blockedX, blockedY) + if blockedLoss > 0 { + e2eharness.ConfirmedBugf(t, 27602, + "Lunatic Gaze drained %d sanity through the brain room geometry (clear line drained %d)", + blockedLoss, clearLoss) + } + t.Logf("PASS Lunatic Gaze LoS: clear drained %d, blocked drained %d", clearLoss, blockedLoss) +} diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp index 9cc6b150da..634bec4679 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp @@ -2586,14 +2586,17 @@ class spell_yogg_saron_lunatic_gaze : public SpellScript void FilterTargets(std::list& targets) { - std::list tmplist; - for (std::list::iterator itr = targets.begin(); itr != targets.end(); ++itr) - if ((*itr)->HasInArc(M_PI, GetCaster())) - tmplist.push_back(*itr); + Unit* caster = GetCaster(); + // 64168 inherits SPELL_ATTR2_IGNORE_LINE_OF_SIGHT from the aura triggering it, so the illusion room walls have to be checked here + bool ignoreLos = GetSpellInfo()->HasAttribute(SPELL_ATTR2_IGNORE_LINE_OF_SIGHT); - targets.clear(); - for (std::list::iterator itr = tmplist.begin(); itr != tmplist.end(); ++itr) - targets.push_back(*itr); + targets.remove_if([caster, ignoreLos](WorldObject* target) + { + if (!target->HasInArc(M_PI, caster)) + return true; + + return !ignoreLos && !caster->IsWithinLOSInMap(target, VMAP::ModelIgnoreFlags::M2); + }); } void Register() override