mirror of
https://gitcode.com/GitHub_Trending/az/azerothcore-wotlk.git
synced 2026-10-10 07:06:38 +08:00
fix(Scripts/TrialOfTheCrusader): Don't despawn Gormok's Snobolds when he dies (#27942)
This commit is contained in:
@@ -166,6 +166,7 @@ go test -tags=e2e ./local/... -count=1 -v -timeout 30m -parallel 1
|
||||
| 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/classic/onyxias_lair | Onyxia's phase 2 whelps follow a retail sniff: 19 at liftoff (ten rookery points, a liftoff-only point, and the eight back-row eggs each new whelp hatches, 2s later), then each point respawns 30-60s after its own whelp dies and the eggs hatch again, 18 in wave two; no whelp casts Teleport Self at a bot it can walk to; at 40% she lands straight down in about 10s, first Bellowing Roar 2s after touchdown, and each landing Roar sets off the Lava Fissures near her, each fissure casting Eruption itself, and only a fissure above the lair floor passing it on to its neighbours in the same tick, so a Roar erupts a cluster around her and never the whole floor, with a crack animation per hit, never before a Roar lands and never twice inside a fissure's 10s cooldown | P2 | covered (`TestOnyxia_WhelpCadence`, `TestOnyxia_EruptionFollowsBellowingRoar`) | — |
|
||||
| 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; 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; a Kick that lands on Flame Leviathan stops his Flame Vents channel | P2 | covered (`TestAC_27095_*`, `TestAC_27602_*`, `TestAC_27539_*`, `TestAC_27455_*`, `TestUlduar_BrightleafSunBeamsCappedPerWave`, `TestUlduar_FreyaWardLasherOutlivesSummonDuration`, `TestUlduar_ThorimEvadeDespawnDoesNotYield`, `TestUlduar_FreyaGiftMatchesElderCount`, `TestAC_27736_IgnisKillsAllConstructs`, `TestUlduar_AlgalonWipeRespawnsOnce`, `TestUlduar_FlameVentsInterruptedByKick`); 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 #27602 #27736 |
|
||||
| instances/northrend/trial_of_the_crusader | the Snobold Vassals riding Gormok outlive him, each engages the raid within seconds of his death, and they despawn once the raid wipes | P2 | covered (`TestToC_GormokSnoboldsFightAfterDeath`) | #27942 |
|
||||
| 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,188 @@
|
||||
//go:build e2e
|
||||
|
||||
package trial_of_the_crusader_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/azerothcore/AzerothGhost/e2e/e2eharness"
|
||||
"github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta"
|
||||
)
|
||||
|
||||
// PR: https://github.com/azerothcore/azerothcore-wotlk/pull/27942
|
||||
//
|
||||
// Gormok carries four Snobold Vassals as vehicle accessories. His death ejects them, and the
|
||||
// boss script used to despawn every Snobold that was not riding a player right after. Per sniff
|
||||
// they stay on the ground, keep bombing and join the fight a few seconds after landing.
|
||||
//
|
||||
// Drive: a temp Gormok is summoned in the arena (his intro needs Barrett's gossip, which the
|
||||
// harness cannot send) and killed with `.damage`, which ignores his NON_ATTACKABLE flag.
|
||||
// Oracle: every Snobold that rode him is still alive well after his death, each then targets
|
||||
// the bot, and once the bot dies they all despawn instead of being left behind in the arena.
|
||||
func TestToC_GormokSnoboldsFightAfterDeath(t *testing.T) {
|
||||
meta.Begin(t, meta.TestMeta{
|
||||
Tags: []string{"med", "instances"},
|
||||
Runtime: "med",
|
||||
Category: "instances/northrend/trial_of_the_crusader",
|
||||
})
|
||||
|
||||
const (
|
||||
mapTrialOfTheCrusader = uint32(649)
|
||||
npcGormok = uint32(34796)
|
||||
npcSnobold = uint32(34800)
|
||||
npcTirion = uint32(34996)
|
||||
|
||||
// Arena centre (trial_of_the_crusader.h LOC_CENTER).
|
||||
centerX, centerY, centerZ = float32(563.67), float32(139.57), float32(393.84)
|
||||
|
||||
seatWindow = 15 * time.Second
|
||||
// Snobolds must outlive Gormok's death handling, which runs in the same tick.
|
||||
surviveWindow = 3 * time.Second
|
||||
// Dismounted Snobolds engage 5s after Gormok dies.
|
||||
engageWindow = 12 * time.Second
|
||||
despawnWindow = 15 * time.Second
|
||||
sampleEvery = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{
|
||||
Prefix: "GrmSnb", Race: e2eharness.RaceHuman,
|
||||
Class: e2eharness.ClassWarrior, Level: 80, LearnAllClass: true,
|
||||
})
|
||||
|
||||
// Stay GM through the raid enter (.go xyz onto a raid map is ignored after .gm off).
|
||||
bot.Teleport(t, centerX, centerY, centerZ, mapTrialOfTheCrusader)
|
||||
if _, _, _, m := bot.Pos(); m != mapTrialOfTheCrusader {
|
||||
e2eharness.Preconditionf(t, "bot not in Trial of the Crusader after tele map=%d", m)
|
||||
}
|
||||
|
||||
known := map[uint64]struct{}{}
|
||||
for _, u := range bot.UnitsByEntry(0, npcGormok) {
|
||||
known[u.GUID] = struct{}{}
|
||||
}
|
||||
e2eharness.SpawnNPC(t, bot.World, npcGormok)
|
||||
fresh := bot.WaitNewUnits(t, known, []uint32{npcGormok}, 20*time.Second)
|
||||
if len(fresh) == 0 {
|
||||
e2eharness.Preconditionf(t, "no new Gormok after .npc add temp")
|
||||
}
|
||||
gormok := fresh[0].GUID
|
||||
t.Logf("Gormok guid=0x%X", gormok)
|
||||
t.Cleanup(func() {
|
||||
if hp, _ := bot.UnitHP(gormok); hp == 0 {
|
||||
return
|
||||
}
|
||||
if err := bot.World.SetTarget(gormok); err == nil {
|
||||
bot.GM(t, ".npc delete")
|
||||
}
|
||||
})
|
||||
|
||||
// The harness stores a passenger's seat offset as its position, which puts a seated Snobold
|
||||
// near (0,0,0), hundreds of yards from the bot; search wide and match on the GUID's entry.
|
||||
snobolds := func() []uint64 {
|
||||
var out []uint64
|
||||
for _, u := range bot.NearbyUnits(2000) {
|
||||
if uint32((u.GUID>>24)&0xFFFFFF) == npcSnobold && u.Health() > 0 {
|
||||
out = append(out, u.GUID)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
var riders []uint64
|
||||
for deadline := time.Now().Add(seatWindow); time.Now().Before(deadline); time.Sleep(sampleEvery) {
|
||||
if riders = snobolds(); len(riders) == 4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(riders) == 0 {
|
||||
e2eharness.Preconditionf(t, "no Snobold Vassal seated on Gormok 0x%X within %s", gormok, seatWindow)
|
||||
}
|
||||
t.Logf("%d Snobolds riding Gormok: %v", len(riders), guidList(riders))
|
||||
|
||||
// Out of GM mode so the Snobolds can pick the bot; god keeps it up until the wipe below.
|
||||
e2eharness.CombatReady(t, bot.World, e2eharness.CombatReadyOpts{God: true})
|
||||
bot.DamageKill(t, []uint64{gormok}, 0, 15*time.Second)
|
||||
killedAt := time.Now()
|
||||
t.Logf("Gormok dead")
|
||||
|
||||
for time.Since(killedAt) < surviveWindow {
|
||||
for _, g := range riders {
|
||||
if hp, _ := bot.UnitHP(g); bot.World.GetObject(g) == nil || hp == 0 {
|
||||
e2eharness.Assertf(t, "Snobold 0x%X gone %s after Gormok died; his riders must stay and fight",
|
||||
g, time.Since(killedAt).Round(time.Millisecond))
|
||||
}
|
||||
}
|
||||
time.Sleep(sampleEvery)
|
||||
}
|
||||
t.Logf("all %d Snobolds still alive %s after Gormok died", len(riders), surviveWindow)
|
||||
|
||||
me := bot.World.CharGUID()
|
||||
pending := map[uint64]struct{}{}
|
||||
for _, g := range riders {
|
||||
pending[g] = struct{}{}
|
||||
}
|
||||
for deadline := killedAt.Add(engageWindow); len(pending) > 0 && time.Now().Before(deadline); time.Sleep(sampleEvery) {
|
||||
for g := range pending {
|
||||
if bot.UnitTarget(g) == me {
|
||||
t.Logf("Snobold 0x%X engaged the bot %s after Gormok died", g, time.Since(killedAt).Round(time.Millisecond))
|
||||
delete(pending, g)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(pending) > 0 {
|
||||
var left []uint64
|
||||
for g := range pending {
|
||||
left = append(left, g)
|
||||
}
|
||||
e2eharness.Assertf(t, "%d Snobold(s) %v never targeted the bot within %s of Gormok's death",
|
||||
len(left), guidList(left), engageWindow)
|
||||
}
|
||||
|
||||
// Tirion watches from the stands and stays put through the wipe, so his absence afterwards
|
||||
// would mean the bot simply lost its view of the arena, not that the Snobolds despawned.
|
||||
tirion := bot.World.FindUnitByEntry(npcTirion, 0)
|
||||
if tirion == 0 {
|
||||
e2eharness.Preconditionf(t, "no Tirion (%d) in the object cache to vouch for the despawn check", npcTirion)
|
||||
}
|
||||
|
||||
// The wipe: with nobody left to fight they evade, and a dismounted Snobold despawns on evade.
|
||||
bot.DieMust(t, 25*time.Second)
|
||||
diedAt := time.Now()
|
||||
for deadline := diedAt.Add(despawnWindow); time.Now().Before(deadline); time.Sleep(sampleEvery) {
|
||||
if len(present(bot, riders)) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if left := present(bot, riders); len(left) > 0 {
|
||||
e2eharness.Assertf(t, "%d Snobold(s) %v still in the arena %s after the wipe; dismounted Snobolds must despawn on evade",
|
||||
len(left), guidList(left), despawnWindow)
|
||||
}
|
||||
if bot.World.GetObject(tirion) == nil {
|
||||
e2eharness.HarnessFailf(t, "Tirion 0x%X left the object cache with the Snobolds; the despawn check proves nothing", tirion)
|
||||
}
|
||||
t.Logf("PASS %d Snobolds outlived Gormok, engaged the bot and despawned %s after the wipe",
|
||||
len(riders), time.Since(diedAt).Round(time.Millisecond))
|
||||
}
|
||||
|
||||
func present(bot *e2eharness.ScenarioBot, guids []uint64) []uint64 {
|
||||
var out []uint64
|
||||
for _, g := range guids {
|
||||
if bot.World.GetObject(g) != nil {
|
||||
out = append(out, g)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func guidList(guids []uint64) string {
|
||||
s := ""
|
||||
for i, g := range guids {
|
||||
if i > 0 {
|
||||
s += " "
|
||||
}
|
||||
s += fmt.Sprintf("0x%X", g)
|
||||
}
|
||||
return s
|
||||
}
|
||||
+88
-31
@@ -50,6 +50,12 @@ enum GormokEvents
|
||||
EVENT_SPELL_BATTER,
|
||||
EVENT_SPELL_FIRE_BOMB,
|
||||
EVENT_SPELL_HEAD_CRACK,
|
||||
EVENT_DISMOUNTED_ATTACK,
|
||||
};
|
||||
|
||||
enum GormokActions
|
||||
{
|
||||
ACTION_GORMOK_DIED = 1,
|
||||
};
|
||||
|
||||
enum GormokNPCs
|
||||
@@ -96,6 +102,7 @@ public:
|
||||
InstanceScript* pInstance;
|
||||
EventMap events;
|
||||
ObjectGuid TargetGUID;
|
||||
bool Dismounted = false;
|
||||
|
||||
void Reset() override
|
||||
{
|
||||
@@ -105,6 +112,9 @@ public:
|
||||
|
||||
void JustEngagedWith(Unit* /*who*/) override
|
||||
{
|
||||
if (Dismounted)
|
||||
return;
|
||||
|
||||
events.Reset();
|
||||
events.ScheduleEvent(EVENT_SPELL_SNOBOLLED, 1500ms);
|
||||
events.ScheduleEvent(EVENT_SPELL_BATTER, 5s);
|
||||
@@ -113,41 +123,77 @@ public:
|
||||
|
||||
void AttackStart(Unit* who) override
|
||||
{
|
||||
if (who->GetGUID() != TargetGUID )
|
||||
if (!Dismounted && who->GetGUID() != TargetGUID)
|
||||
return;
|
||||
ScriptedAI::AttackStart(who);
|
||||
}
|
||||
|
||||
void MoveInLineOfSight(Unit* /*who*/) override {}
|
||||
|
||||
void EnterEvadeMode(EvadeReason why) override
|
||||
{
|
||||
// Nothing cleans up a dismounted snobold once Gormok's corpse is gone
|
||||
if (Dismounted)
|
||||
{
|
||||
me->DespawnOrUnsummon();
|
||||
return;
|
||||
}
|
||||
|
||||
ScriptedAI::EnterEvadeMode(why);
|
||||
}
|
||||
|
||||
void LoseCarrier(Unit* carrier)
|
||||
{
|
||||
if (carrier)
|
||||
carrier->RemoveAura(SPELL_CHANGE_VEHICLE);
|
||||
me->RemoveAllAuras();
|
||||
me->GetThreatMgr().ClearAllThreat();
|
||||
me->CombatStop(true);
|
||||
me->SetHealth(me->GetMaxHealth());
|
||||
TargetGUID.Clear();
|
||||
Creature* gormok = pInstance ? ObjectAccessor::GetCreature(*me, pInstance->GetGuidData(TYPE_GORMOK)) : nullptr;
|
||||
if (gormok && gormok->IsAlive())
|
||||
{
|
||||
if (Vehicle* vk = gormok->GetVehicleKit())
|
||||
for (uint8 i = 0; i < 4; ++i)
|
||||
if (!vk->GetPassenger(i))
|
||||
{
|
||||
me->EnterVehicleUnattackable(gormok, i);
|
||||
Reset();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else // Gormok is dead or gone, so fight on like the Snobolds ejected from him
|
||||
DoAction(ACTION_GORMOK_DIED);
|
||||
}
|
||||
|
||||
void UpdateAI(uint32 diff) override
|
||||
{
|
||||
if (!TargetGUID && !me->GetVehicle())
|
||||
return;
|
||||
|
||||
Unit* t = ObjectAccessor::GetUnit(*me, TargetGUID);
|
||||
if (!t && !(t = me->GetVehicleBase()))
|
||||
return;
|
||||
|
||||
if (t->isDead())
|
||||
Unit* t = nullptr;
|
||||
if (Dismounted)
|
||||
{
|
||||
t->RemoveAura(SPELL_CHANGE_VEHICLE);
|
||||
me->RemoveAllAuras();
|
||||
me->GetThreatMgr().ClearAllThreat();
|
||||
me->CombatStop(true);
|
||||
me->SetHealth(me->GetMaxHealth());
|
||||
if (pInstance)
|
||||
if (Creature* gormok = ObjectAccessor::GetCreature(*me, pInstance->GetGuidData(TYPE_GORMOK)))
|
||||
if (gormok->IsAlive())
|
||||
if (Vehicle* vk = gormok->GetVehicleKit())
|
||||
for( uint8 i = 0; i < 4; ++i )
|
||||
if (!vk->GetPassenger(i))
|
||||
{
|
||||
me->EnterVehicleUnattackable(gormok, i);
|
||||
Reset();
|
||||
break;
|
||||
}
|
||||
TargetGUID.Clear();
|
||||
if (me->GetReactState() != REACT_PASSIVE && !UpdateVictim())
|
||||
return;
|
||||
|
||||
t = me->GetVictim();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TargetGUID && !me->GetVehicle())
|
||||
return;
|
||||
|
||||
t = ObjectAccessor::GetUnit(*me, TargetGUID);
|
||||
if (!t && !(t = me->GetVehicleBase()))
|
||||
{
|
||||
// The carrier left the map, which ejects its passengers
|
||||
LoseCarrier(nullptr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Dismounted && t->isDead())
|
||||
{
|
||||
LoseCarrier(t);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -160,6 +206,11 @@ public:
|
||||
{
|
||||
case 0:
|
||||
break;
|
||||
case EVENT_DISMOUNTED_ATTACK:
|
||||
me->SetReactState(REACT_AGGRESSIVE);
|
||||
DoZoneInCombat();
|
||||
events.ScheduleEvent(EVENT_SPELL_HEAD_CRACK, 1s, 5s);
|
||||
break;
|
||||
case EVENT_SPELL_SNOBOLLED:
|
||||
if (t->IsPlayer())
|
||||
me->CastSpell((Unit*)nullptr, SPELL_SNOBOLLED, true);
|
||||
@@ -172,7 +223,7 @@ public:
|
||||
break;
|
||||
case EVENT_SPELL_FIRE_BOMB:
|
||||
{
|
||||
if (!t->IsPlayer() && pInstance )
|
||||
if ((Dismounted || !t->IsPlayer()) && pInstance)
|
||||
{
|
||||
GuidVector validPlayers;
|
||||
Map::PlayerList const& pl = me->GetMap()->GetPlayers();
|
||||
@@ -181,7 +232,7 @@ public:
|
||||
for( Map::PlayerList::const_iterator itr = pl.begin(); itr != pl.end(); ++itr )
|
||||
{
|
||||
if (Player* p = itr->GetSource())
|
||||
if (p->IsAlive() && p->GetGUID() != TargetGUID && (!gormok || !p->IsWithinMeleeRange(gormok)))
|
||||
if (p->IsAlive() && p->GetGUID() != TargetGUID && (!gormok || !gormok->IsAlive() || !p->IsWithinMeleeRange(gormok)))
|
||||
validPlayers.push_back(p->GetGUID());
|
||||
}
|
||||
|
||||
@@ -219,8 +270,14 @@ public:
|
||||
|
||||
void DoAction(int32 param) override
|
||||
{
|
||||
if (param == 1 && !TargetGUID)
|
||||
me->DespawnOrUnsummon();
|
||||
// Gormok's death ejects his passengers; they keep bombing and join the fight shortly after landing
|
||||
if (param != ACTION_GORMOK_DIED || TargetGUID)
|
||||
return;
|
||||
|
||||
Dismounted = true;
|
||||
events.Reset();
|
||||
events.ScheduleEvent(EVENT_SPELL_FIRE_BOMB, 1s, 12s);
|
||||
events.ScheduleEvent(EVENT_DISMOUNTED_ATTACK, 5s);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -381,7 +438,7 @@ public:
|
||||
|
||||
void JustDied(Unit* /*pKiller*/) override
|
||||
{
|
||||
summons.DoAction(1);
|
||||
summons.DoAction(ACTION_GORMOK_DIED);
|
||||
|
||||
if (pInstance)
|
||||
pInstance->SetData(TYPE_GORMOK, DONE);
|
||||
|
||||
Reference in New Issue
Block a user