diff --git a/Source/Game/AI/CoreAI/CreatureAI.cs b/Source/Game/AI/CoreAI/CreatureAI.cs index 2740b4e59..c8ffcff81 100644 --- a/Source/Game/AI/CoreAI/CreatureAI.cs +++ b/Source/Game/AI/CoreAI/CreatureAI.cs @@ -493,6 +493,7 @@ namespace Game.AI public virtual void SpellHitTarget(Unit target, SpellInfo spellInfo) { } public virtual void SpellHitTargetGameObject(GameObject target, SpellInfo spellInfo) { } + // Should return true if the NPC is currently being escorted public virtual bool IsEscorted() { return false; } public virtual void MovementInform(MovementGeneratorType type, uint id) { } @@ -565,14 +566,6 @@ namespace Game.AI // Object destruction is handled by Unit::RemoveCharmedBy public virtual PlayerAI GetAIForCharmedPlayer(Player who) { return null; } - /// - /// Should return true if the NPC is target of an escort quest - /// If onlyIfActive is set, should return true only if the escort quest is currently active - /// - /// - /// - public virtual bool IsEscortNPC(bool onlyIfActive) { return false; } - public List GetBoundary() { return _boundary; } public bool IsEngaged() { return _isEngaged; } diff --git a/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs b/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs index 2c253b8ca..438267232 100644 --- a/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs +++ b/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs @@ -382,15 +382,8 @@ namespace Game.AI CreatureData cdata = me.GetCreatureData(); if (cdata != null) { - SpawnGroupTemplateData groupdata = cdata.spawnGroupData; - if (groupdata != null) - { - if (WorldConfig.GetBoolValue(WorldCfg.RespawnDynamicEscortNpc) && groupdata.flags.HasAnyFlag(SpawnGroupFlags.EscortQuestNpc) && map.GetCreatureRespawnTime(me.GetSpawnId()) == 0) - { - me.SetRespawnTime(me.GetRespawnDelay()); - me.SaveRespawnTime(); - } - } + if (WorldConfig.GetBoolValue(WorldCfg.RespawnDynamicEscortNpc) && cdata.spawnGroupData.flags.HasFlag(SpawnGroupFlags.EscortQuestNpc)) + me.SaveRespawnTime(me.GetRespawnDelay()); } } @@ -467,21 +460,10 @@ namespace Game.AI } } - public override bool IsEscortNPC(bool onlyIfActive) - { - if (!onlyIfActive) - return true; - - if (!GetEventStarterGUID().IsEmpty()) - return true; - - return false; - } - void SetPauseTimer(uint Timer) { _pauseTimer = Timer; } public bool HasEscortState(EscortState escortState) { return (_escortState & escortState) != 0; } - public override bool IsEscorted() { return _escortState.HasAnyFlag(EscortState.Escorting); } + public override bool IsEscorted() { return !_playerGUID.IsEmpty(); } void SetMaxPlayerDistance(float newMax) { _maxPlayerDistance = newMax; } float GetMaxPlayerDistance() { return _maxPlayerDistance; } diff --git a/Source/Game/AI/ScriptedAI/ScriptedFollowerAI.cs b/Source/Game/AI/ScriptedAI/ScriptedFollowerAI.cs index 322593203..30212edbc 100644 --- a/Source/Game/AI/ScriptedAI/ScriptedFollowerAI.cs +++ b/Source/Game/AI/ScriptedAI/ScriptedFollowerAI.cs @@ -18,19 +18,19 @@ using Framework.Constants; using Game.Entities; using Game.Groups; +using Game.Maps; using System; namespace Game.AI { enum FollowState { - None = 0x000, - Inprogress = 0x001, //must always have this state for any follow - Returning = 0x002, //when returning to combat start after being in combat - Paused = 0x004, //disables following - Complete = 0x008, //follow is completed and may end - PreEvent = 0x010, //not implemented (allow pre event to run, before follow is initiated) - PostEvent = 0x020 //can be set at complete and allow post event to run + None = 0x00, + Inprogress = 0x01, //must always have this state for any follow + Paused = 0x02, //disables following + Complete = 0x04, //follow is completed and may end + PreEvent = 0x08, //not implemented (allow pre event to run, before follow is initiated) + PostEvent = 0x10 //can be set at complete and allow post event to run } class FollowerAI : ScriptedAI @@ -38,45 +38,17 @@ namespace Game.AI ObjectGuid _leaderGUID; uint _updateFollowTimer; FollowState _followState; - - Quest _questForFollow; //normally we have a quest + uint _questForFollow; public FollowerAI(Creature creature) : base(creature) { _updateFollowTimer = 2500; _followState = FollowState.None; - _questForFollow = null; - } - - public override void MovementInform(MovementGeneratorType motionType, uint pointId) - { - if (motionType != MovementGeneratorType.Point || !HasFollowState(FollowState.Inprogress)) - return; - - if (pointId == 0xFFFFFF) - { - if (GetLeaderForFollower()) - { - if (!HasFollowState(FollowState.Paused)) - AddFollowState(FollowState.Returning); - } - else - me.DespawnOrUnsummon(); - } - } - - public override void AttackStart(Unit who) - { - base.AttackStart(who); } public override void MoveInLineOfSight(Unit who) { - // TODO: what in the world is this? - if (me.HasReactState(ReactStates.Aggressive) && !me.HasUnitState(UnitState.Stunned) && who.IsTargetableForAttack() && who.IsInAccessiblePlaceFor(me)) - return; - - if (HasFollowState(FollowState.Inprogress) && AssistPlayerInCombatAgainst(who)) + if (HasFollowState(FollowState.Inprogress) && !ShouldAssistPlayerInCombatAgainst(who)) return; base.MoveInLineOfSight(who); @@ -84,7 +56,7 @@ namespace Game.AI public override void JustDied(Unit killer) { - if (!HasFollowState(FollowState.Inprogress) || _leaderGUID.IsEmpty() || _questForFollow == null) + if (!HasFollowState(FollowState.Inprogress) || _leaderGUID.IsEmpty() || _questForFollow == 0) return; // @todo need a better check for quests with time limit. @@ -99,59 +71,39 @@ namespace Game.AI Player member = groupRef.GetSource(); if (member) if (member.IsInMap(player)) - member.FailQuest(_questForFollow.Id); + member.FailQuest(_questForFollow); } } else - player.FailQuest(_questForFollow.Id); + player.FailQuest(_questForFollow); } } - public override void JustAppeared() + public override void JustReachedHome() { - _followState = FollowState.None; - - if (!IsCombatMovementAllowed()) - SetCombatMovement(true); - - if (me.GetFaction() != me.GetCreatureTemplate().Faction) - me.SetFaction(me.GetCreatureTemplate().Faction); - - Reset(); - } - - public override void EnterEvadeMode(EvadeReason why) - { - if (!me.IsAlive()) - { - EngagementOver(); + if (!HasFollowState(FollowState.Inprogress)) return; - } - me.RemoveAllAuras(); - me.CombatStop(true); - me.SetLootRecipient(null); - me.SetCannotReachTarget(false); - me.DoNotReacquireTarget(); - - EngagementOver(); - - if (HasFollowState(FollowState.Inprogress)) + Player player = GetLeaderForFollower(); + if (player != null) { - Log.outDebug(LogFilter.Scripts, "FollowerAI left combat, returning to CombatStartPosition."); - - if (me.HasUnitState(UnitState.Chase)) - me.GetMotionMaster().Remove(MovementGeneratorType.Chase); + if (HasFollowState(FollowState.Paused)) + return; + me.GetMotionMaster().MoveFollow(player, SharedConst.PetFollowDist, SharedConst.PetFollowAngle); } else - me.GetMotionMaster().MoveTargetedHome(); + me.DespawnOrUnsummon(); + } - Reset(); + public override void OwnerAttackedBy(Unit attacker) + { + if (!me.HasReactState(ReactStates.Passive) && ShouldAssistPlayerInCombatAgainst(attacker)) + me.EngageWithTarget(attacker); } public override void UpdateAI(uint uiDiff) { - if (HasFollowState(FollowState.Inprogress) && !me.GetVictim()) + if (HasFollowState(FollowState.Inprogress) && !me.IsEngaged()) { if (_updateFollowTimer <= uiDiff) { @@ -162,43 +114,49 @@ namespace Game.AI return; } - bool bIsMaxRangeExceeded = true; + bool maxRangeExceeded = true; + bool questAbandoned = (_questForFollow != 0); Player player = GetLeaderForFollower(); if (player) { - if (HasFollowState(FollowState.Returning)) - { - Log.outDebug(LogFilter.Scripts, "FollowerAI is returning to leader."); - - RemoveFollowState(FollowState.Returning); - me.GetMotionMaster().MoveFollow(player, SharedConst.PetFollowDist, SharedConst.PetFollowAngle); - return; - } - Group group = player.GetGroup(); if (group) { - for (GroupReference groupRef = group.GetFirstMember(); groupRef != null; groupRef = groupRef.Next()) + for (GroupReference groupRef = group.GetFirstMember(); groupRef != null && (maxRangeExceeded || questAbandoned); groupRef = groupRef.Next()) { Player member = groupRef.GetSource(); - if (member && me.IsWithinDistInMap(member, 100.0f)) + if (member == null) + continue; + + if (maxRangeExceeded && me.IsWithinDistInMap(member, 100.0f)) + maxRangeExceeded = false; + + if (questAbandoned) { - bIsMaxRangeExceeded = false; - break; + QuestStatus status = member.GetQuestStatus(_questForFollow); + if ((status == QuestStatus.Complete) || (status == QuestStatus.Incomplete)) + questAbandoned = false; } } } else { if (me.IsWithinDistInMap(player, 100.0f)) - bIsMaxRangeExceeded = false; + maxRangeExceeded = false; + + if (questAbandoned) + { + QuestStatus status = player.GetQuestStatus(_questForFollow); + if ((status == QuestStatus.Complete) || (status == QuestStatus.Incomplete)) + questAbandoned = false; + } } } - if (bIsMaxRangeExceeded) + if (maxRangeExceeded || questAbandoned) { - Log.outDebug(LogFilter.Scripts, "FollowerAI failed because player/group was to far away or not found"); + Log.outDebug(LogFilter.Scripts, $"FollowerAI::UpdateAI: failed because player/group was to far away or not found ({me.GetGUID()})"); me.DespawnOrUnsummon(); return; } @@ -222,15 +180,26 @@ namespace Game.AI public void StartFollow(Player player, uint factionForFollower = 0, Quest quest = null) { - if (me.GetVictim()) + Map map = me.GetMap(); + if (map != null) { - Log.outDebug(LogFilter.Scripts, "FollowerAI attempt to StartFollow while in combat."); + CreatureData cdata = me.GetCreatureData(); + if (cdata != null) + { + if (WorldConfig.GetBoolValue(WorldCfg.RespawnDynamicEscortNpc) && cdata.spawnGroupData.flags.HasFlag(SpawnGroupFlags.EscortQuestNpc)) + me.SaveRespawnTime(me.GetRespawnDelay()); + } + } + + if (me.IsEngaged()) + { + Log.outDebug(LogFilter.Scripts, $"FollowerAI::StartFollow: attempt to StartFollow while in combat. ({me.GetGUID()})"); return; } if (HasFollowState(FollowState.Inprogress)) { - Log.outError(LogFilter.Scenario, "FollowerAI attempt to StartFollow while already following."); + Log.outError(LogFilter.Scenario, $"FollowerAI::StartFollow: attempt to StartFollow while already following. ({me.GetGUID()})"); return; } @@ -240,7 +209,7 @@ namespace Game.AI if (factionForFollower != 0) me.SetFaction(factionForFollower); - _questForFollow = quest; + _questForFollow = quest.Id; me.GetMotionMaster().Clear(MovementGeneratorPriority.Normal); me.PauseMovement(); @@ -252,7 +221,7 @@ namespace Game.AI me.GetMotionMaster().MoveFollow(player, SharedConst.PetFollowDist, SharedConst.PetFollowAngle); - Log.outDebug(LogFilter.Scripts, "FollowerAI start follow {0} ({1})", player.GetName(), _leaderGUID.ToString()); + Log.outDebug(LogFilter.Scripts, $"FollowerAI::StartFollow: start follow {player.GetName()} - {_leaderGUID} ({me.GetGUID()})"); } public void SetFollowPaused(bool paused) @@ -310,7 +279,7 @@ namespace Game.AI Player member = groupRef.GetSource(); if (member && me.IsWithinDistInMap(member, 100.0f) && member.IsAlive()) { - Log.outDebug(LogFilter.Scripts, "FollowerAI GetLeader changed and returned new leader."); + Log.outDebug(LogFilter.Scripts, $"FollowerAI::GetLeaderForFollower: GetLeader changed and returned new leader. ({me.GetGUID()})"); _leaderGUID = member.GetGUID(); return member; } @@ -319,29 +288,22 @@ namespace Game.AI } } - Log.outDebug(LogFilter.Scripts, "FollowerAI GetLeader can not find suitable leader."); + Log.outDebug(LogFilter.Scripts, $"FollowerAI::GetLeaderForFollower: GetLeader can not find suitable leader. ({me.GetGUID()})"); return null; } //This part provides assistance to a player that are attacked by who, even if out of normal aggro range //It will cause me to attack who that are attacking _any_ player (which has been confirmed may happen also on offi) //The flag (type_flag) is unconfirmed, but used here for further research and is a good candidate. - bool AssistPlayerInCombatAgainst(Unit who) + bool ShouldAssistPlayerInCombatAgainst(Unit who) { if (!who || !who.GetVictim()) return false; - if (me.HasReactState(ReactStates.Passive)) - return false; - //experimental (unknown) flag not present if (!me.GetCreatureTemplate().TypeFlags.HasAnyFlag(CreatureTypeFlags.CanAssist)) return false; - //not a player - if (!who.GetVictim().GetCharmerOrOwnerPlayerOrPlayerItself()) - return false; - if (!who.IsInAccessiblePlaceFor(me)) return false; @@ -361,18 +323,18 @@ namespace Game.AI return false; //too far away and no free sight? - if (me.IsWithinDistInMap(who, 100.0f) && me.IsWithinLOSInMap(who)) - { - me.EngageWithTarget(who); - return true; - } + if (!me.IsWithinDistInMap(who, 100.0f) || !me.IsWithinLOSInMap(who)) + return false; - return false; + return true; } + public override bool IsEscorted() { return HasFollowState(FollowState.Inprogress); } + bool HasFollowState(FollowState uiFollowState) { return (_followState & uiFollowState) != 0; } void AddFollowState(FollowState uiFollowState) { _followState |= uiFollowState; } + void RemoveFollowState(FollowState uiFollowState) { _followState &= ~uiFollowState; } } } diff --git a/Source/Game/Entities/Creature/Creature.cs b/Source/Game/Entities/Creature/Creature.cs index c86b8cd62..47998763a 100644 --- a/Source/Game/Entities/Creature/Creature.cs +++ b/Source/Game/Entities/Creature/Creature.cs @@ -1100,11 +1100,11 @@ namespace Game.Entities } } - public bool IsEscortNPC(bool onlyIfActive = true) + public bool IsEscorted() { CreatureAI ai = GetAI(); if (ai != null) - return ai.IsEscortNPC(onlyIfActive); + return ai.IsEscorted(); return false; } @@ -2302,8 +2302,10 @@ namespace Game.Entities if (!victim.IsInAccessiblePlaceFor(this)) return false; - if (IsAIEnabled() && !GetAI().CanAIAttack(victim)) - return false; + CreatureAI ai = GetAI(); + if (ai != null) + if (!ai.CanAIAttack(victim)) + return false; // we cannot attack in evade mode if (IsInEvadeMode()) diff --git a/Source/Game/Maps/Map.cs b/Source/Game/Maps/Map.cs index 21b0d2532..05f757e80 100644 --- a/Source/Game/Maps/Map.cs +++ b/Source/Game/Maps/Map.cs @@ -2348,7 +2348,7 @@ namespace Game.Maps continue; // escort NPCs are allowed to respawn as long as all other instances are already escorting - if (isEscort && creature.IsEscortNPC(true)) + if (isEscort && creature.IsEscorted()) continue; doDelete = true;