Scripts/FollowerAI: Some cleanup:

- FollowerAI properly resumes follow after evading.
- Removed duplicated getters from CreatureAI (IsEscorted vs IsEscortNPC), they were used to do the same thing
- FollowerAI properly assists in combat.
- FollowerAI properly despawns if quest is abandoned.
- FollowerAI now supports dynamic respawning for escort NPCs.
This commit is contained in:
Treeston
2020-01-31 07:03:51 +01:00
committed by Ovahlord
parent eaa635ab32
commit e156f8d571
6 changed files with 120 additions and 232 deletions

View File

@@ -140,6 +140,7 @@ class TC_GAME_API CreatureAI : public UnitAI
// Called when spell hits a target
virtual void SpellHitTarget(Unit* /*target*/, SpellInfo const* /*spell*/) { }
// Should return true if the NPC is currently being escorted
virtual bool IsEscorted() const { return false; }
// Called when creature appears in the world (spawn, respawn, grid load etc...)
@@ -197,9 +198,6 @@ class TC_GAME_API CreatureAI : public UnitAI
// If a PlayerAI* is returned, that AI is placed on the player instead of the default charm AI
// Object destruction is handled by Unit::RemoveCharmedBy
virtual PlayerAI* GetAIForCharmedPlayer(Player* /*who*/) { return nullptr; }
// 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
virtual bool IsEscortNPC(bool /*onlyIfActive*/) const { return false; }
// intended for encounter design/debugging. do not use for other purposes. expensive.
int32 VisualizeBoundary(uint32 duration, Unit* owner = nullptr, bool fill = false) const;

View File

@@ -371,14 +371,8 @@ void EscortAI::Start(bool isActiveAttacker /* = true*/, bool run /* = false */,
{
if (CreatureData const* cdata = me->GetCreatureData())
{
if (SpawnGroupTemplateData const* groupdata = cdata->spawnGroupData)
{
if (sWorld->getBoolConfig(CONFIG_RESPAWN_DYNAMIC_ESCORTNPC) && (groupdata->flags & SPAWNGROUP_FLAG_ESCORTQUESTNPC) && !map->GetCreatureRespawnTime(me->GetSpawnId()))
{
me->SetRespawnTime(me->GetRespawnDelay());
me->SaveRespawnTime();
}
}
if (sWorld->getBoolConfig(CONFIG_RESPAWN_DYNAMIC_ESCORTNPC) && (cdata->spawnGroupData->flags & SPAWNGROUP_FLAG_ESCORTQUESTNPC))
me->SaveRespawnTime(me->GetRespawnDelay());
}
}
@@ -454,14 +448,3 @@ void EscortAI::SetEscortPaused(bool on)
_resume = true;
}
}
bool EscortAI::IsEscortNPC(bool onlyIfActive) const
{
if (!onlyIfActive)
return true;
if (GetEventStarterGUID())
return true;
return false;
}

View File

@@ -50,29 +50,21 @@ struct TC_GAME_API EscortAI : public ScriptedAI
virtual void UpdateEscortAI(uint32 diff); // used when it's needed to add code in update (abilities, scripted events, etc)
void AddWaypoint(uint32 id, float x, float y, float z, float orientation = 0.f, uint32 waitTime = 0); // waitTime is in ms
void Start(bool isActiveAttacker = true, bool run = false, ObjectGuid playerGUID = ObjectGuid::Empty, Quest const* quest = nullptr, bool instantRespawn = false, bool canLoopPath = false, bool resetWaypoints = true);
void SetRun(bool on = true);
void SetEscortPaused(bool on);
void SetPauseTimer(uint32 Timer) { _pauseTimer = Timer; }
bool HasEscortState(uint32 escortState) { return (_escortState & escortState) != 0; }
virtual bool IsEscorted() const override { return (_escortState & STATE_ESCORT_ESCORTING); }
bool IsEscorted() const override { return !_playerGUID.IsEmpty(); }
void SetMaxPlayerDistance(float newMax) { _maxPlayerDistance = newMax; }
float GetMaxPlayerDistance() const { return _maxPlayerDistance; }
void SetDespawnAtEnd(bool despawn) { _despawnAtEnd = despawn; }
void SetDespawnAtFar(bool despawn) { _despawnAtFar = despawn; }
bool GetAttack() const { return _activeAttacker; } // used in EnterEvadeMode override
void SetCanAttack(bool attack) { _activeAttacker = attack; }
ObjectGuid GetEventStarterGUID() const { return _playerGUID; }
virtual bool IsEscortNPC(bool isEscorting) const override;
protected:
Player* GetPlayerForEscort();

View File

@@ -38,110 +38,19 @@ enum Points
POINT_COMBAT_START = 0xFFFFFF
};
FollowerAI::FollowerAI(Creature* creature) : ScriptedAI(creature),
m_uiUpdateFollowTimer(2500),
m_uiFollowState(STATE_FOLLOW_NONE),
m_pQuestForFollow(nullptr)
{ }
void FollowerAI::AttackStart(Unit* who)
{
if (!who)
return;
if (me->Attack(who, true))
{
me->AddThreat(who, 0.0f);
me->SetInCombatWith(who);
who->SetInCombatWith(me);
if (me->HasUnitState(UNIT_STATE_FOLLOW))
me->ClearUnitState(UNIT_STATE_FOLLOW);
if (IsCombatMovementAllowed())
me->GetMotionMaster()->MoveChase(who);
}
}
//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 FollowerAI::AssistPlayerInCombatAgainst(Unit* who)
{
if (!who || !who->GetVictim())
return false;
//experimental (unknown) flag not present
if (!(me->GetCreatureTemplate()->type_flags & CREATURE_TYPE_FLAG_CAN_ASSIST))
return false;
//not a player
if (!who->EnsureVictim()->GetCharmerOrOwnerPlayerOrPlayerItself())
return false;
//never attack friendly
if (me->IsFriendlyTo(who))
return false;
//too far away and no free sight?
if (me->IsWithinDistInMap(who, MAX_PLAYER_DISTANCE) && me->IsWithinLOSInMap(who))
{
//already fighting someone?
if (!me->GetVictim())
{
AttackStart(who);
return true;
}
else
{
who->SetInCombatWith(me);
me->AddThreat(who, 0.0f);
return true;
}
}
return false;
}
FollowerAI::FollowerAI(Creature* creature) : ScriptedAI(creature), _updateFollowTimer(2500), _followState(STATE_FOLLOW_NONE), _questForFollow(0) { }
void FollowerAI::MoveInLineOfSight(Unit* who)
{
if (me->HasReactState(REACT_AGGRESSIVE) && !me->HasUnitState(UNIT_STATE_STUNNED) && who->isTargetableForAttack() && who->isInAccessiblePlaceFor(me))
{
if (HasFollowState(STATE_FOLLOW_INPROGRESS) && AssistPlayerInCombatAgainst(who))
return;
if (HasFollowState(STATE_FOLLOW_INPROGRESS) && !ShouldAssistPlayerInCombatAgainst(who))
return;
if (!me->CanFly() && me->GetDistanceZ(who) > CREATURE_Z_ATTACK_RANGE)
return;
if (me->IsHostileTo(who))
{
float fAttackRadius = me->GetAttackDistance(who);
if (me->IsWithinDistInMap(who, fAttackRadius) && me->IsWithinLOSInMap(who))
{
if (!me->GetVictim())
{
// Clear distracted state on combat
if (me->HasUnitState(UNIT_STATE_DISTRACTED))
{
me->ClearUnitState(UNIT_STATE_DISTRACTED);
me->GetMotionMaster()->Clear();
}
AttackStart(who);
}
else if (me->GetMap()->IsDungeon())
{
who->SetInCombatWith(me);
me->AddThreat(who, 0.0f);
}
}
}
}
ScriptedAI::MoveInLineOfSight(who);
}
void FollowerAI::JustDied(Unit* /*killer*/)
{
if (!HasFollowState(STATE_FOLLOW_INPROGRESS) || !m_uiLeaderGUID || !m_pQuestForFollow)
if (!HasFollowState(STATE_FOLLOW_INPROGRESS) || !_leaderGUID || !_questForFollow)
return;
/// @todo need a better check for quests with time limit.
@@ -152,58 +61,38 @@ void FollowerAI::JustDied(Unit* /*killer*/)
for (GroupReference* groupRef = group->GetFirstMember(); groupRef != nullptr; groupRef = groupRef->next())
if (Player* member = groupRef->GetSource())
if (member->IsInMap(player))
member->FailQuest(m_pQuestForFollow->GetQuestId());
member->FailQuest(_questForFollow);
}
else
player->FailQuest(m_pQuestForFollow->GetQuestId());
player->FailQuest(_questForFollow);
}
}
void FollowerAI::JustAppeared()
void FollowerAI::JustReachedHome()
{
m_uiFollowState = STATE_FOLLOW_NONE;
if (!HasFollowState(STATE_FOLLOW_INPROGRESS))
return;
if (!IsCombatMovementAllowed())
SetCombatMovement(true);
if (me->GetFaction() != me->GetCreatureTemplate()->faction)
me->SetFaction(me->GetCreatureTemplate()->faction);
Reset();
}
void FollowerAI::EnterEvadeMode(EvadeReason /*why*/)
{
me->RemoveAllAuras();
me->DeleteThreatList();
me->CombatStop(true);
me->SetLootRecipient(nullptr);
if (HasFollowState(STATE_FOLLOW_INPROGRESS))
if (Player* player = GetLeaderForFollower())
{
TC_LOG_DEBUG("scripts", "FollowerAI left combat, returning to CombatStartPosition.");
if (me->GetMotionMaster()->GetCurrentMovementGeneratorType() == CHASE_MOTION_TYPE)
{
float fPosX, fPosY, fPosZ;
me->GetPosition(fPosX, fPosY, fPosZ);
me->GetMotionMaster()->MovePoint(POINT_COMBAT_START, fPosX, fPosY, fPosZ);
}
if (HasFollowState(STATE_FOLLOW_PAUSED))
return;
me->GetMotionMaster()->MoveFollow(player, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE);
}
else
{
if (me->GetMotionMaster()->GetCurrentMovementGeneratorType() == CHASE_MOTION_TYPE)
me->GetMotionMaster()->MoveTargetedHome();
}
Reset();
me->DespawnOrUnsummon();
}
void FollowerAI::OwnerAttackedBy(Unit* other)
{
if (!me->HasReactState(REACT_PASSIVE) && ShouldAssistPlayerInCombatAgainst(other))
AttackStart(other);
}
void FollowerAI::UpdateAI(uint32 uiDiff)
{
if (HasFollowState(STATE_FOLLOW_INPROGRESS) && !me->GetVictim())
{
if (m_uiUpdateFollowTimer <= uiDiff)
if (_updateFollowTimer <= uiDiff)
{
if (HasFollowState(STATE_FOLLOW_COMPLETE) && !HasFollowState(STATE_FOLLOW_POSTEVENT))
{
@@ -212,49 +101,51 @@ void FollowerAI::UpdateAI(uint32 uiDiff)
return;
}
bool bIsMaxRangeExceeded = true;
bool maxRangeExceeded = true;
bool questAbandoned = (_questForFollow != 0);
if (Player* player = GetLeaderForFollower())
{
if (HasFollowState(STATE_FOLLOW_RETURNING))
{
TC_LOG_DEBUG("scripts", "FollowerAI is returning to leader.");
RemoveFollowState(STATE_FOLLOW_RETURNING);
me->GetMotionMaster()->MoveFollow(player, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE);
return;
}
if (Group* group = player->GetGroup())
{
for (GroupReference* groupRef = group->GetFirstMember(); groupRef != nullptr; groupRef = groupRef->next())
for (GroupReference* groupRef = group->GetFirstMember(); groupRef && (maxRangeExceeded || questAbandoned); groupRef = groupRef->next())
{
Player* member = groupRef->GetSource();
if (member && me->IsWithinDistInMap(member, MAX_PLAYER_DISTANCE))
if (!member)
continue;
if (maxRangeExceeded && me->IsWithinDistInMap(member, MAX_PLAYER_DISTANCE))
maxRangeExceeded = false;
if (questAbandoned)
{
bIsMaxRangeExceeded = false;
break;
QuestStatus status = member->GetQuestStatus(_questForFollow);
if ((status == QUEST_STATUS_COMPLETE) || (status == QUEST_STATUS_INCOMPLETE))
questAbandoned = false;
}
}
}
else
{
if (me->IsWithinDistInMap(player, MAX_PLAYER_DISTANCE))
bIsMaxRangeExceeded = false;
maxRangeExceeded = false;
if (questAbandoned)
{
QuestStatus status = player->GetQuestStatus(_questForFollow);
if ((status == QUEST_STATUS_COMPLETE) || (status == QUEST_STATUS_INCOMPLETE))
questAbandoned = false;
}
}
}
if (bIsMaxRangeExceeded)
if (maxRangeExceeded || questAbandoned)
{
TC_LOG_DEBUG("scripts", "FollowerAI failed because player/group was to far away or not found");
TC_LOG_DEBUG("scripts.ai.followerai", "FollowerAI::UpdateAI: failed because player/group was to far away or not found (%s)", me->GetGUID().ToString().c_str());
me->DespawnOrUnsummon();
return;
}
m_uiUpdateFollowTimer = 1000;
_updateFollowTimer = 1000;
}
else
m_uiUpdateFollowTimer -= uiDiff;
_updateFollowTimer -= uiDiff;
}
UpdateFollowerAI(uiDiff);
@@ -268,25 +159,17 @@ void FollowerAI::UpdateFollowerAI(uint32 /*uiDiff*/)
DoMeleeAttackIfReady();
}
void FollowerAI::MovementInform(uint32 motionType, uint32 pointId)
void FollowerAI::StartFollow(Player* player, uint32 factionForFollower, uint32 quest)
{
if (motionType != POINT_MOTION_TYPE || !HasFollowState(STATE_FOLLOW_INPROGRESS))
return;
if (pointId == POINT_COMBAT_START)
if (Map* map = me->GetMap())
{
if (GetLeaderForFollower())
if (CreatureData const* cdata = me->GetCreatureData())
{
if (!HasFollowState(STATE_FOLLOW_PAUSED))
AddFollowState(STATE_FOLLOW_RETURNING);
if (sWorld->getBoolConfig(CONFIG_RESPAWN_DYNAMIC_ESCORTNPC) && (cdata->spawnGroupData->flags & SPAWNGROUP_FLAG_ESCORTQUESTNPC))
me->SaveRespawnTime(me->GetRespawnDelay());
}
else
me->DespawnOrUnsummon();
}
}
void FollowerAI::StartFollow(Player* player, uint32 factionForFollower, const Quest* quest)
{
if (me->GetVictim())
{
TC_LOG_DEBUG("scripts", "FollowerAI attempt to StartFollow while in combat.");
@@ -299,13 +182,13 @@ void FollowerAI::StartFollow(Player* player, uint32 factionForFollower, const Qu
return;
}
//set variables
m_uiLeaderGUID = player->GetGUID();
// set variables
_leaderGUID = player->GetGUID();
if (factionForFollower)
me->SetFaction(factionForFollower);
m_pQuestForFollow = quest;
_questForFollow = quest;
if (me->GetMotionMaster()->GetCurrentMovementGeneratorType() == WAYPOINT_MOTION_TYPE)
{
@@ -320,12 +203,12 @@ void FollowerAI::StartFollow(Player* player, uint32 factionForFollower, const Qu
me->GetMotionMaster()->MoveFollow(player, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE);
TC_LOG_DEBUG("scripts", "FollowerAI start follow %s (%s)", player->GetName().c_str(), m_uiLeaderGUID.ToString().c_str());
TC_LOG_DEBUG("scripts.ai.followerai", "FollowerAI::StartFollow: start follow %s - %s (%s)", player->GetName().c_str(), _leaderGUID.ToString().c_str(), me->GetGUID().ToString().c_str());
}
Player* FollowerAI::GetLeaderForFollower()
{
if (Player* player = ObjectAccessor::GetPlayer(*me, m_uiLeaderGUID))
if (Player* player = ObjectAccessor::GetPlayer(*me, _leaderGUID))
{
if (player->IsAlive())
return player;
@@ -339,7 +222,7 @@ Player* FollowerAI::GetLeaderForFollower()
if (member && me->IsWithinDistInMap(member, MAX_PLAYER_DISTANCE) && member->IsAlive())
{
TC_LOG_DEBUG("scripts", "FollowerAI GetLeader changed and returned new leader.");
m_uiLeaderGUID = member->GetGUID();
_leaderGUID = member->GetGUID();
return member;
}
}
@@ -399,3 +282,37 @@ void FollowerAI::SetFollowPaused(bool paused)
me->GetMotionMaster()->MoveFollow(leader, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE);
}
}
bool FollowerAI::ShouldAssistPlayerInCombatAgainst(Unit* who) const
{
if (!who || !who->GetVictim())
return false;
// experimental (unknown) flag not present
if (!(me->GetCreatureTemplate()->type_flags & CREATURE_TYPE_FLAG_CAN_ASSIST))
return false;
if (!who->isInAccessiblePlaceFor(me))
return false;
if (!CanAIAttack(who))
return false;
// we cannot attack in evade mode
if (me->IsInEvadeMode())
return false;
// or if enemy is in evade mode
if (who->GetTypeId() == TYPEID_UNIT && who->ToCreature()->IsInEvadeMode())
return false;
// never attack friendly
if (me->IsFriendlyTo(who))
return false;
// too far away and no free sight
if (!me->IsWithinDistInMap(who, MAX_PLAYER_DISTANCE) || !me->IsWithinLOSInMap(who))
return false;
return true;
}

View File

@@ -20,18 +20,18 @@
#include "ScriptedCreature.h"
#include "ScriptSystem.h"
#include "World.h"
class Quest;
enum eFollowState
enum FollowerState : uint32
{
STATE_FOLLOW_NONE = 0x000,
STATE_FOLLOW_INPROGRESS = 0x001, //must always have this state for any follow
STATE_FOLLOW_RETURNING = 0x002, //when returning to combat start after being in combat
STATE_FOLLOW_PAUSED = 0x004, //disables following
STATE_FOLLOW_COMPLETE = 0x008, //follow is completed and may end
STATE_FOLLOW_PREEVENT = 0x010, //not implemented (allow pre event to run, before follow is initiated)
STATE_FOLLOW_POSTEVENT = 0x020 //can be set at complete and allow post event to run
STATE_FOLLOW_INPROGRESS = 0x001, // must always have this state for any follow
STATE_FOLLOW_PAUSED = 0x002, // disables following
STATE_FOLLOW_COMPLETE = 0x004, // follow is completed and may end
STATE_FOLLOW_PREEVENT = 0x008, // not implemented (allow pre event to run, before follow is initiated)
STATE_FOLLOW_POSTEVENT = 0x010 // can be set at complete and allow post event to run
};
class TC_GAME_API FollowerAI : public ScriptedAI
@@ -40,42 +40,38 @@ class TC_GAME_API FollowerAI : public ScriptedAI
explicit FollowerAI(Creature* creature);
~FollowerAI() { }
void MovementInform(uint32 motionType, uint32 pointId) override;
void AttackStart(Unit*) override;
void MoveInLineOfSight(Unit*) override;
void EnterEvadeMode(EvadeReason /*why*/ = EVADE_REASON_OTHER) override;
void JustDied(Unit*) override;
void JustReachedHome() override;
void OwnerAttackedBy(Unit* other) override;
void JustAppeared() override;
//the "internal" update, calls UpdateFollowerAI()
void UpdateAI(uint32) override;
void UpdateAI(uint32) override; //the "internal" update, calls UpdateFollowerAI()
virtual void UpdateFollowerAI(uint32); //used when it's needed to add code in update (abilities, scripted events, etc)
//used when it's needed to add code in update (abilities, scripted events, etc)
virtual void UpdateFollowerAI(uint32);
void StartFollow(Player* player, uint32 factionForFollower = 0, Quest const* quest = nullptr);
void StartFollow(Player* player, uint32 factionForFollower = 0, uint32 quest = 0);
void SetFollowPaused(bool bPaused); //if special event require follow mode to hold/resume during the follow
void SetFollowComplete(bool bWithEndEvent = false);
//if special event require follow mode to hold/resume during the follow
void SetFollowPaused(bool paused);
void SetFollowComplete(bool withEndEvent = false);
bool HasFollowState(uint32 uiFollowState) { return (m_uiFollowState & uiFollowState) != 0; }
bool IsEscorted() const override { return HasFollowState(STATE_FOLLOW_INPROGRESS); }
bool HasFollowState(uint32 followState) const { return (_followState & followState) != 0; }
protected:
Player* GetLeaderForFollower();
private:
void AddFollowState(uint32 uiFollowState) { m_uiFollowState |= uiFollowState; }
void RemoveFollowState(uint32 uiFollowState) { m_uiFollowState &= ~uiFollowState; }
void AddFollowState(uint32 followState) { _followState |= followState; }
void RemoveFollowState(uint32 followState) { _followState &= ~followState; }
bool ShouldAssistPlayerInCombatAgainst(Unit* who) const;
bool AssistPlayerInCombatAgainst(Unit* who);
ObjectGuid m_uiLeaderGUID;
uint32 m_uiUpdateFollowTimer;
uint32 m_uiFollowState;
Quest const* m_pQuestForFollow; //normally we have a quest
ObjectGuid _leaderGUID;
uint32 _updateFollowTimer;
uint32 _followState;
uint32 _questForFollow;
};
#endif

View File

@@ -57,6 +57,7 @@ void AddSC_tele_commandscript();
void AddSC_ticket_commandscript();
void AddSC_titles_commandscript();
void AddSC_wp_commandscript();
void AddSC_dev_commandscript();
// The name of this function should match:
// void Add${NameOfDirectory}Scripts()
@@ -103,4 +104,5 @@ void AddCommandsScripts()
AddSC_ticket_commandscript();
AddSC_titles_commandscript();
AddSC_wp_commandscript();
AddSC_dev_commandscript();
}