diff --git a/data/sql/updates/pending_db_world/rev_1787188287042735114.sql b/data/sql/updates/pending_db_world/rev_1787188287042735114.sql new file mode 100644 index 0000000000..3d9922a2a1 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1787188287042735114.sql @@ -0,0 +1,68 @@ +-- +-- Timmy the Cruel +SET @CGUID := 247227; +SET @PATH := @CGUID * 10; + +UPDATE `creature` SET `position_x` = 3624.6, `position_y` = -3188, `position_z` = 130.579, `orientation` = 2.99341, `MovementType` = 0, `VerifiedBuild` = 53622, `CreateObject` = 2 WHERE `guid` = @CGUID AND `id` = 10808; + +DELETE FROM `creature_addon` WHERE `guid` = @CGUID; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES + (@CGUID, 0, 0, 0, 1, 0, 3, '12787'); + +DELETE FROM `waypoint_data` WHERE `id` = @PATH; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES + (@PATH, 1, 3622.5618, -3187.6912, 130.70120, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 2, 3637.9067, -3196.1228, 128.94208, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 3, 3653.7864, -3202.9502, 127.63325, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 4, 3674.5347, -3204.5970, 126.66777, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 5, 3689.5625, -3191.0405, 127.16926, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 6, 3696.2886, -3171.0410, 127.19991, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 7, 3694.8628, -3156.6287, 127.44449, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 8, 3681.7678, -3154.1362, 127.26646, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 9, 3677.8403, -3162.5703, 126.69397, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 10, 3676.1355, -3175.5400, 126.43660, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 11, 3668.1090, -3185.7537, 126.311264, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 12, 3673.8386, -3194.0088, 126.40000, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 13, 3667.7388, -3203.3460, 126.56412, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 14, 3656.9858, -3204.5164, 127.65019, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 15, 3651.1753, -3196.8152, 127.30198, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 16, 3658.5908, -3188.9165, 126.71776, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 17, 3669.3274, -3183.5960, 126.359184, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 18, 3678.2393, -3171.0085, 126.507866, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 19, 3668.1770, -3166.6174, 126.90177, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 20, 3653.5696, -3174.7020, 127.15160, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 21, 3645.5012, -3185.9297, 127.69186, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 22, 3634.0906, -3178.5388, 129.00195, NULL, 0, 0, 0, 0, 0, 100, 0), + (@PATH, 23, 3623.9392, -3180.2332, 130.64621, NULL, 0, 0, 0, 0, 0, 100, 0); + +DELETE FROM `creature_template_spell` WHERE `CreatureID` = 10808; +INSERT INTO `creature_template_spell` (`CreatureID`, `Index`, `Spell`) VALUES + (10808, 0, 12787), + (10808, 1, 17470), + (10808, 2, 8599); + +DELETE FROM `smart_scripts` WHERE `entryorguid` = 10808 AND `source_type` = 0; +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_param6`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES + (10808, 0, 0, 0, 0, 0, 100, 0, 10000, 15000, 10000, 15000, 0, 0, 11, 17470, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - In Combat - Cast Ravenous Claw'), + (10808, 0, 1, 0, 2, 0, 100, 1, 0, 50, 0, 0, 0, 0, 11, 8599, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - Between 0-50% Health - Cast Enrage'), + (10808, 0, 2, 0, 4, 0, 100, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - On Aggro - Say Line 0'), + (10808, 0, 3, 4, 11, 0, 100, 0, 0, 0, 0, 0, 0, 0, 47, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - On Respawn - Set Invisible'), + (10808, 0, 4, 5, 61, 0, 100, 0, 0, 0, 0, 0, 0, 0, 18, 768, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - On Respawn - Set Immune to Players and NPCs'), + (10808, 0, 5, 0, 61, 0, 100, 0, 0, 0, 0, 0, 0, 0, 22, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - On Respawn - Set Event Phase 1'), + (10808, 0, 6, 0, 1, 1, 100, 1, 10000, 10000, 10000, 10000, 0, 0, 80, 1080800, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - Out of Combat - Run Emerge Actionlist'); + +DELETE FROM `conditions` WHERE `SourceTypeOrReferenceId` = 22 AND `SourceEntry` = 10808; +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES + (22, 7, 10808, 0, 1, 29, 1, 10418, 70, 0, 1, 0, 0, '', 'Timmy the Cruel - Activate if no living Crimson Guardsman is within 70 yards'), + (22, 7, 10808, 0, 1, 29, 1, 10419, 70, 0, 1, 0, 0, '', 'Timmy the Cruel - Activate if no living Crimson Conjuror is within 70 yards'), + (22, 7, 10808, 0, 1, 29, 1, 10420, 70, 0, 1, 0, 0, '', 'Timmy the Cruel - Activate if no living Crimson Initiate is within 70 yards'), + (22, 7, 10808, 0, 1, 29, 1, 10424, 70, 0, 1, 0, 0, '', 'Timmy the Cruel - Activate if no living Crimson Gallant is within 70 yards'); + +DELETE FROM `smart_scripts` WHERE `entryorguid` = 1080800 AND `source_type` = 9; +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_param6`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES + (1080800, 9, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 17, 449, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - Actionlist - Set Emerge Emote State'), + (1080800, 9, 1, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - Actionlist - Set Visible'), + (1080800, 9, 2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 19, 768, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - Actionlist - Remove Immunity to Players and NPCs'), + (1080800, 9, 3, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - Actionlist - Set Event Phase 0'), + (1080800, 9, 4, 0, 0, 0, 100, 0, 3500, 3500, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - Actionlist - Clear Emote State'), + (1080800, 9, 5, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 232, @PATH, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Timmy the Cruel - Actionlist - Start Repeating Path'); diff --git a/e2e/README.md b/e2e/README.md index 879d5d86f6..1cb4a83438 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -155,6 +155,7 @@ If the scenario should stay as a regression, **move** it into `suites/` next to | protocol/teleport | cross-map; named; GoCreatureID | P1 | covered | — | | 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 | --- diff --git a/e2e/suites/instances/classic/stratholme/stratholme_e2e_test.go b/e2e/suites/instances/classic/stratholme/stratholme_e2e_test.go new file mode 100644 index 0000000000..ac56520cbb --- /dev/null +++ b/e2e/suites/instances/classic/stratholme/stratholme_e2e_test.go @@ -0,0 +1,158 @@ +//go:build e2e + +package stratholme_test + +import ( + "testing" + "time" + + _ "github.com/go-sql-driver/mysql" + + "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" +) + +const ( + npcTimmyTheCruel = uint32(10808) + npcCrimsonInitiate = uint32(10420) + + stratholmeMap = uint32(329) + triggerRadius = float32(55) + activationCheckEvery = 10 * time.Second +) + +var timmyActivationEntries = []uint32{10418, 10419, 10420, 10424} + +func waitForTimmyActivationSet(t *testing.T, bot *e2eharness.ScenarioBot, timeout time.Duration) []uint64 { + t.Helper() + + want := map[uint32]int{ + 10418: 8, + 10419: 3, + 10420: 2, + 10424: 2, + } + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + timer := time.NewTimer(timeout) + defer timer.Stop() + + var got map[uint32]int + for { + got = e2eharness.CountLivingByEntry(bot.World, triggerRadius, timmyActivationEntries...) + complete := true + for entry, count := range want { + if got[entry] != count { + complete = false + break + } + } + if complete { + return e2eharness.LivingByEntries(bot.World, triggerRadius, timmyActivationEntries...) + } + + select { + case <-ticker.C: + case <-timer.C: + e2eharness.Preconditionf(t, "Timmy activation area did not load completely within %s: got=%v want=%v", timeout, got, want) + return nil + } + } +} + +func assertTimmyAbsentWhileAlive(t *testing.T, bot *e2eharness.ScenarioBot, survivor uint64, window time.Duration) { + t.Helper() + + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + timer := time.NewTimer(window) + defer timer.Stop() + + for { + if health, _ := bot.UnitHP(survivor); health == 0 { + e2eharness.Preconditionf(t, "final relevant Scarlet 0x%X died before the gating oracle completed", survivor) + } + if timmy := bot.FindUnit(npcTimmyTheCruel, 100); timmy != 0 { + e2eharness.ConfirmedBugf(t, 26363, "Timmy emerged as 0x%X while relevant Scarlet 0x%X was still alive", timmy, survivor) + } + + select { + case <-ticker.C: + case <-timer.C: + return + } + } +} + +func waitForTimmy(t *testing.T, bot *e2eharness.ScenarioBot, timeout time.Duration) uint64 { + t.Helper() + + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + timer := time.NewTimer(timeout) + defer timer.Stop() + + for { + if timmy := bot.FindUnit(npcTimmyTheCruel, 100); timmy != 0 { + return timmy + } + + select { + case <-ticker.C: + case <-timer.C: + e2eharness.ConfirmedBugf(t, 26363, "Timmy did not emerge within %s after the final Scarlet trigger died", timeout) + return 0 + } + } +} + +// Issue: https://github.com/azerothcore/azerothcore-wotlk/issues/26363 +// Timmy must remain hidden while any relevant Scarlet in Crusaders' Square is +// alive, then emerge on the next SmartAI activation check. +func TestAC_26363_TimmyEmergesAfterSquareCleared(t *testing.T) { + meta.Begin(t, meta.TestMeta{ + Tags: []string{"med", "instances", "issue", "serial"}, + Runtime: "med", + Issue: 26363, + Category: "instances/classic/stratholme", + }) + + bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{ + Prefix: "Timmy", + Level: 80, + }) + + // Center of the 15 relevant Scarlet spawns in Crusaders' Square. The 55-yard + // observation radius excludes other Scarlet spawns with the same entries. + bot.Teleport(t, 3660, -3180, 127, stratholmeMap) + triggers := waitForTimmyActivationSet(t, bot, 20*time.Second) + if len(triggers) != 15 { + e2eharness.Preconditionf(t, "loaded Timmy activation area has %d relevant Scarlets, want 15", len(triggers)) + } + bot.CombatReady(t) + if timmy := bot.FindUnit(npcTimmyTheCruel, 100); timmy != 0 { + e2eharness.ConfirmedBugf(t, 26363, "Timmy was already present as 0x%X before the Scarlet activation area was cleared", timmy) + } + + // The previous implementation omitted Crimson Initiates from its conditions. + // Leave one Initiate alive through a complete activation interval so the test + // deterministically guards that regression instead of choosing any survivor. + initiates := e2eharness.LivingByEntries(bot.World, triggerRadius, npcCrimsonInitiate) + if len(initiates) != 2 { + e2eharness.Preconditionf(t, "loaded Timmy activation area has %d Crimson Initiates, want 2", len(initiates)) + } + survivor := initiates[0] + victims := make([]uint64, 0, len(triggers)-1) + for _, guid := range triggers { + if guid != survivor { + victims = append(victims, guid) + } + } + bot.DamageKill(t, victims, 10_000_000, 20*time.Second) + assertTimmyAbsentWhileAlive(t, bot, survivor, activationCheckEvery+2*time.Second) + + bot.DamageKill(t, []uint64{survivor}, 10_000_000, 10*time.Second) + timmy := waitForTimmy(t, bot, 15*time.Second) + bot.AssertWorldAlive(t) + t.Logf("PASS AC#26363 Timmy emerged as 0x%X only after all 15 relevant Scarlets died", timmy) +}