diff --git a/sql/pending_updates/world/from_the_shadows.sql b/sql/pending_updates/world/from_the_shadows.sql new file mode 100644 index 0000000000..5cbf6d91af --- /dev/null +++ b/sql/pending_updates/world/from_the_shadows.sql @@ -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; diff --git a/sql/pending_updates/world/gilnean_mastiff_recall_trigger.sql b/sql/pending_updates/world/gilnean_mastiff_recall_trigger.sql new file mode 100644 index 0000000000..b35847d993 --- /dev/null +++ b/sql/pending_updates/world/gilnean_mastiff_recall_trigger.sql @@ -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); diff --git a/sql/updates/world/2026_08_13_world_07.sql b/sql/updates/world/2026_08_13_world_07.sql index 2c9d7ee26e..a014bca036 100644 --- a/sql/updates/world/2026_08_13_world_07.sql +++ b/sql/updates/world/2026_08_13_world_07.sql @@ -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; diff --git a/src/server/scripts/EasternKingdoms/Gilneas/gilneas.cpp b/src/server/scripts/EasternKingdoms/Gilneas/gilneas.cpp index af16533d09..4ae614f97b 100644 --- a/src/server/scripts/EasternKingdoms/Gilneas/gilneas.cpp +++ b/src/server/scripts/EasternKingdoms/Gilneas/gilneas.cpp @@ -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 @@ -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 players; + Skyfire::AnyPlayerInObjectRangeCheck checker(me, 8.0f); + Skyfire::PlayerListSearcher 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(); }