Implement From the Shadows and fix the duplicate cellar door auto-close

Adds Lorna Crowley's quest-accept handler, the Gilnean Mastiff tracking
pet with a collar-triggered ability to reveal and engage stealthed
Bloodfang Lurkers, and a recall trigger to work around a navmesh gap
in the cellar stairwell. Also stops the surviving cellar door from
auto-closing after use.
This commit is contained in:
AlterEgo 2026-08-13 14:58:22 -05:00
parent e44797e0f7
commit c80bc663b7
No known key found for this signature in database
GPG key ID: AEEED62276D1412A
4 changed files with 360 additions and 0 deletions

View file

@ -0,0 +1,8 @@
UPDATE creature_template SET ScriptName='npc_lorna_crowley_p4' WHERE entry=35378;
UPDATE creature_template SET ScriptName='npc_gilnean_mastiff' WHERE entry=35631;
UPDATE creature_template SET ScriptName='npc_bloodfang_lurker' WHERE entry=35463;
UPDATE creature_template SET npcflag=0 WHERE entry=35463;
DELETE FROM creature_queststarter WHERE id=35463 AND quest=14204;
UPDATE item_template SET spellid_1=0, spelltrigger_1=0 WHERE entry=48707;

View file

@ -0,0 +1,18 @@
DELETE FROM creature_template WHERE entry=900000;
INSERT INTO creature_template
SELECT 900000, difficulty_entry_1, difficulty_entry_2, difficulty_entry_3, KillCredit1, KillCredit2,
modelid1, modelid2, modelid3, modelid4, 'Gilnean Mastiff Recall Trigger', '', IconName, gossip_menu_id,
minlevel, maxlevel, exp, exp_unk, faction_A, faction_H, npcflag, speed_walk, speed_run, scale, npc_rank, mindmg, maxdmg,
dmgschool, attackpower, dmg_multiplier, baseattacktime, rangeattacktime, unit_class, unit_flags, unit_flags2,
dynamicflags, family, trainer_type, trainer_class, trainer_race, minrangedmg, maxrangedmg,
rangedattackpower, type, type_flags, type_flags2, lootid, pickpocketloot, skinloot, resistance1, resistance2,
resistance3, resistance4, resistance5, resistance6, spell1, spell2, spell3, spell4, spell5, spell6, spell7,
spell8, PetSpellDataId, VehicleId, mingold, maxgold, AIName, MovementType, InhabitType, HoverHeight, Health_mod,
Mana_mod, Mana_mod_extra, Armor_mod, RacialLeader, questItem1, questItem2, questItem3, questItem4, questItem5, questItem6,
movementId, RegenHealth, mechanic_immune_mask, flags_extra, 'npc_gilnean_mastiff_recall_trigger',
ModLevel, WDBVerified
FROM creature_template WHERE entry=35374;
DELETE FROM creature WHERE id=900000;
INSERT INTO creature (id, map, spawnMask, phaseId, phaseGroup, modelid, equipment_id, position_x, position_y, position_z, orientation, spawntimesecs, spawndist, currentwaypoint, curhealth, curmana, MovementType, npcflag, unit_flags, dynamicflags)
VALUES (900000, 654, 1, 171, 0, 0, 0, -1790.120361, 1438.133423, 20.060425, 0, 300, 0, 0, 1, 0, 0, 0, 0, 0);

View file

@ -1,3 +1,5 @@
DELETE FROM gameobject WHERE guid=90379;
DELETE FROM game_event_gameobject WHERE guid=90379;
UPDATE gameobject_template SET data0=0 WHERE entry=195430;
UPDATE gameobject_template SET data2=0 WHERE entry=195621;

View file

@ -6,9 +6,12 @@
#include "ScriptMgr.h"
#include "ScriptedCreature.h"
#include "GameObjectAI.h"
#include "GridNotifiers.h"
#include "GridNotifiersImpl.h"
#include "ObjectAccessor.h"
#include "Player.h"
#include "SharedDefines.h"
#include "TemporarySummon.h"
#include "Util.h"
#include <unordered_set>
@ -815,6 +818,330 @@ public:
}
};
/*######
## Quest 14204 - From the Shadows
######*/
enum FromTheShadows
{
NPC_GILNEAN_MASTIFF = 35631,
NPC_BLOODFANG_LURKER = 35463,
QUEST_FROM_THE_SHADOWS = 14204,
ITEM_GILNEAN_MASTIFF_COLLAR = 48707,
SPELL_SUMMON_GILNEAN_MASTIFF = 67807,
SPELL_MASTIFF_ATTACK_LURKER = 67805,
SPELL_LURKER_SHADOWSTALKER_STEALTH = 34189,
SPELL_LURKER_ENRAGE = 8599,
CD_LURKER_ENRAGE = 30000,
};
/*######
## npc_lorna_crowley_p4 - quest 14204 accept; clears a hunter's active pet and
## charms the tracking mastiff to the player immediately. The collar item
## (granted natively via the quest's SourceItemId) is then used to command
## the mastiff to expose and attack the nearest stealthed Bloodfang Lurker.
######*/
class npc_lorna_crowley_p4 : public CreatureScript
{
public:
npc_lorna_crowley_p4() : CreatureScript("npc_lorna_crowley_p4") { }
bool OnQuestAccept(Player* player, Creature* /*creature*/, Quest const* quest) OVERRIDE
{
if (quest->GetQuestId() != QUEST_FROM_THE_SHADOWS)
return true;
if (player->getClass() == CLASS_HUNTER && player->GetPet())
player->RemovePet(NULL, PET_SAVE_NOT_IN_SLOT, true);
player->CastSpell(player, SPELL_SUMMON_GILNEAN_MASTIFF, true);
return true;
}
};
/*######
## npc_gilnean_mastiff - the player's tracking pet for quest 14204; strips the
## Bloodfang Lurkers' stealth on command so the player can engage them
######*/
class npc_gilnean_mastiff : public CreatureScript
{
public:
npc_gilnean_mastiff() : CreatureScript("npc_gilnean_mastiff") { }
struct npc_gilnean_mastiffAI : public ScriptedAI
{
npc_gilnean_mastiffAI(Creature* creature) : ScriptedAI(creature), _followCheckTimer(0) { }
uint32 _followCheckTimer;
void Reset() OVERRIDE
{
me->GetCharmInfo()->InitEmptyActionBar(false);
me->GetCharmInfo()->SetActionBar(0, SPELL_MASTIFF_ATTACK_LURKER, ACT_DISABLED);
me->SetReactState(REACT_DEFENSIVE);
me->GetCharmInfo()->SetIsFollowing(true);
_followCheckTimer = 2000;
// Reset() is also where a stuck-in-evade creature lands every time it
// exits evade mode, and Creature::Update() skips UpdateAI() entirely
// while IsInEvadeMode() is true - so the catch-up teleport has to live
// here too, or a mastiff that keeps evading on a broken path never
// gets a chance to run the check in UpdateAI at all.
if (Unit* owner = me->GetCharmerOrOwner())
{
if (me->GetDistance(owner) > 15.0f)
me->NearTeleportTo(owner->GetPositionX(), owner->GetPositionY(), owner->GetPositionZ(), owner->GetOrientation());
me->GetMotionMaster()->MoveFollow(owner, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE);
}
}
// Triggered by using the Gilnean Mastiff Collar. Player target selection
// can't pick out a stealthed Lurker at all, so a player-issued attack has
// nothing to act on. The mastiff's own FindNearestCreature is a
// server-side lookup and isn't blocked by client-side stealth, so it
// sniffs one out and reveals it directly on the player's command.
bool RevealNearbyLurker()
{
if (me->IsInCombat())
return false;
Creature* lurker = me->FindNearestCreature(NPC_BLOODFANG_LURKER, 15.0f, true);
if (!lurker)
return false;
lurker->RemoveAura(SPELL_LURKER_SHADOWSTALKER_STEALTH);
lurker->AddThreat(me, 1.0f);
me->AddThreat(lurker, 1.0f);
AttackStart(lurker);
return true;
}
void UpdateAI(uint32 diff) OVERRIDE
{
Unit* owner = me->GetCharmerOrOwner();
Player* ownerPlayer = owner ? owner->ToPlayer() : NULL;
if (ownerPlayer && ownerPlayer->GetQuestStatus(QUEST_FROM_THE_SHADOWS) == QUEST_STATUS_REWARDED)
{
me->DespawnOrUnsummon(1);
return;
}
// Stairwells/cellar transitions in this zone have gaps in the static
// navmesh, so a normal pathfound follow can get permanently stuck.
// Checked unconditionally (not just while idle) since a broken path
// can also strand it mid-"chase" with a phantom victim.
if (owner)
{
if (_followCheckTimer <= diff)
{
if (me->GetDistance(owner) > 15.0f)
{
me->NearTeleportTo(owner->GetPositionX(), owner->GetPositionY(), owner->GetPositionZ(), owner->GetOrientation());
if (!me->IsInCombat())
me->GetMotionMaster()->MoveFollow(owner, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE);
}
_followCheckTimer = 2000;
}
else
_followCheckTimer -= diff;
}
if (!UpdateVictim())
{
me->GetCharmInfo()->SetIsFollowing(true);
me->SetReactState(REACT_DEFENSIVE);
if (owner && me->GetMotionMaster()->GetCurrentMovementGeneratorType() != FOLLOW_MOTION_TYPE)
me->GetMotionMaster()->MoveFollow(owner, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE);
return;
}
DoMeleeAttackIfReady();
}
void SpellHitTarget(Unit* target, SpellInfo const* spell) OVERRIDE
{
if (spell->Id != SPELL_MASTIFF_ATTACK_LURKER)
return;
target->RemoveAura(SPELL_LURKER_SHADOWSTALKER_STEALTH);
target->AddThreat(me, 1.0f);
me->AddThreat(target, 1.0f);
AttackStart(target);
}
void JustDied(Unit* /*killer*/) OVERRIDE
{
me->DespawnOrUnsummon(1);
}
void KilledUnit(Unit* /*victim*/) OVERRIDE
{
Reset();
}
};
CreatureAI* GetAI(Creature* creature) const OVERRIDE
{
return new npc_gilnean_mastiffAI(creature);
}
};
/*######
## item_gilnean_mastiff_collar - commands the player's mastiff to reveal and
## attack the nearest Bloodfang Lurker. Handled entirely in script (returning
## true suppresses the native item-use spell cast) so this doesn't depend on
## any spell effect data at all.
######*/
class item_gilnean_mastiff_collar : public ItemScript
{
public:
item_gilnean_mastiff_collar() : ItemScript("item_gilnean_mastiff_collar") { }
bool OnUse(Player* player, Item* /*item*/, SpellCastTargets const& /*targets*/) OVERRIDE
{
Guardian* mastiff = player->GetGuardianPet();
if (!mastiff || mastiff->GetEntry() != NPC_GILNEAN_MASTIFF)
return true;
CAST_AI(npc_gilnean_mastiff::npc_gilnean_mastiffAI, mastiff->AI())->RevealNearbyLurker();
return true;
}
};
/*######
## npc_gilnean_mastiff_recall_trigger - invisible catch-all at the top of the
## cellar stairs; players on 14204 whose mastiff isn't nearby get a fresh one
## re-summoned here, working around the stairwell's broken navmesh
######*/
class npc_gilnean_mastiff_recall_trigger : public CreatureScript
{
public:
npc_gilnean_mastiff_recall_trigger() : CreatureScript("npc_gilnean_mastiff_recall_trigger") { }
struct npc_gilnean_mastiff_recall_triggerAI : public ScriptedAI
{
npc_gilnean_mastiff_recall_triggerAI(Creature* creature) : ScriptedAI(creature), _checkTimer(0) { }
uint32 _checkTimer;
void UpdateAI(uint32 diff) OVERRIDE
{
if (_checkTimer > diff)
{
_checkTimer -= diff;
return;
}
_checkTimer = 1000;
std::list<Player*> players;
Skyfire::AnyPlayerInObjectRangeCheck checker(me, 8.0f);
Skyfire::PlayerListSearcher<Skyfire::AnyPlayerInObjectRangeCheck> searcher(me, players, checker);
me->VisitNearbyWorldObject(8.0f, searcher);
for (Player* player : players)
{
if (player->GetQuestStatus(QUEST_FROM_THE_SHADOWS) != QUEST_STATUS_INCOMPLETE)
continue;
Guardian* pet = player->GetGuardianPet();
if (pet && player->GetDistance(pet) <= 20.0f)
continue;
if (pet)
pet->DespawnOrUnsummon(1);
player->CastSpell(player, SPELL_SUMMON_GILNEAN_MASTIFF, true);
}
}
};
CreatureAI* GetAI(Creature* creature) const OVERRIDE
{
return new npc_gilnean_mastiff_recall_triggerAI(creature);
}
};
/*######
## npc_bloodfang_lurker - stealthed quest kill-target; ambushes a nearby idle
## player once it settles back near its home spot, and can enrage at low health
######*/
class npc_bloodfang_lurker : public CreatureScript
{
public:
npc_bloodfang_lurker() : CreatureScript("npc_bloodfang_lurker") { }
struct npc_bloodfang_lurkerAI : public ScriptedAI
{
npc_bloodfang_lurkerAI(Creature* creature) : ScriptedAI(creature) { }
uint32 _enrageTimer;
uint32 _seekTimer;
bool _willEnrage;
void Reset() OVERRIDE
{
_enrageTimer = 0;
_willEnrage = BsohtRand(0, 1) != 0;
_seekTimer = BsohtRand(5000, 10000);
DoCast(me, SPELL_LURKER_SHADOWSTALKER_STEALTH);
}
void UpdateAI(uint32 diff) OVERRIDE
{
if (_seekTimer <= diff)
{
if (me->IsAlive() && !me->IsInCombat() && me->GetDistance2d(me->GetHomePosition().GetPositionX(), me->GetHomePosition().GetPositionY()) <= 2.0f)
{
if (Player* player = me->SelectNearestPlayer(2.0f))
{
if (!player->IsInCombat())
{
AttackStart(player);
_seekTimer = BsohtRand(5000, 10000);
}
}
}
}
else
_seekTimer -= diff;
if (!UpdateVictim())
return;
if (_enrageTimer <= diff)
{
if (_willEnrage && me->GetHealthPct() <= 30.0f)
{
DoCast(me, SPELL_LURKER_ENRAGE);
_enrageTimer = CD_LURKER_ENRAGE;
}
}
else
_enrageTimer -= diff;
DoMeleeAttackIfReady();
}
};
CreatureAI* GetAI(Creature* creature) const OVERRIDE
{
return new npc_bloodfang_lurkerAI(creature);
}
};
void AddSC_gilneas()
{
new go_merchant_square_door();
@ -826,4 +1153,9 @@ void AddSC_gilneas()
new npc_worgen_alpha_c2();
new npc_josiah_avery();
new npc_josiah_avery_trigger();
new npc_lorna_crowley_p4();
new npc_gilnean_mastiff();
new item_gilnean_mastiff_collar();
new npc_gilnean_mastiff_recall_trigger();
new npc_bloodfang_lurker();
}