diff --git a/src/game/CreatureFollowerAI.cpp b/src/game/CreatureFollowerAI.cpp new file mode 100644 index 0000000000..f29cf65899 --- /dev/null +++ b/src/game/CreatureFollowerAI.cpp @@ -0,0 +1,379 @@ +/* This file is part of the ScriptDev2 Project. See AUTHORS file for Copyright information + * This program is free software licensed under GPL version 2 + * Please see the included DOCS/LICENSE.TXT for more information */ + +#include "Object.h" +#include "Unit.h" +#include "Creature.h" +#include "CreatureAI.h" +#include "GameObject.h" +#include "CreatureFollowerAI.h" + +const float MAX_PLAYER_DISTANCE = 100.0f; + +enum +{ + POINT_COMBAT_START = 0xFFFFFF +}; + +CreatureFollowerAI::CreatureFollowerAI(Creature* pCreature) : CreatureFollowerAI(pCreature), + m_uiUpdateFollowTimer(2500), + m_uiFollowState(STATE_FOLLOW_NONE), + m_pQuestForFollow(NULL) +{} + +void CreatureFollowerAI::AttackStart(Unit* pWho) +{ + if (!pWho) + return; + + if (m_creature->Attack(pWho, true)) + { + m_creature->AddThreat(pWho); + m_creature->SetInCombatWith(pWho); + pWho->SetInCombatWith(m_creature); + + if (IsCombatMovement()) + m_creature->GetMotionMaster()->MoveChase(pWho); + } +} + +// This part provides assistance to a player that are attacked by pWho, even if out of normal aggro range +// It will cause m_creature to attack pWho that are attacking _any_ player (which has been confirmed may happen also on offi) +bool CreatureFollowerAI::AssistPlayerInCombat(Unit* pWho) +{ + if (!pWho->getVictim()) + return false; + + // experimental (unknown) flag not present + if (!(m_creature->GetCreatureInfo()->CreatureTypeFlags & CREATURE_TYPEFLAGS_CAN_ASSIST)) + return false; + + // unit state prevents (similar check is done in CanInitiateAttack which also include checking unit_flags. We skip those here) + if (m_creature->hasUnitState(UNIT_STAT_STUNNED | UNIT_STAT_DIED)) + return false; + + // victim of pWho is not a player + if (!pWho->getVictim()->GetCharmerOrOwnerPlayerOrPlayerItself()) + return false; + + // never attack friendly + if (m_creature->IsFriendlyTo(pWho)) + return false; + + // too far away and no free sight? + if (m_creature->IsWithinDistInMap(pWho, MAX_PLAYER_DISTANCE) && m_creature->IsWithinLOSInMap(pWho)) + { + // already fighting someone? + if (!m_creature->getVictim()) + { + AttackStart(pWho); + return true; + } + else + { + pWho->SetInCombatWith(m_creature); + m_creature->AddThreat(pWho); + return true; + } + } + + return false; +} + +void CreatureFollowerAI::MoveInLineOfSight(Unit* pWho) +{ + if (pWho->isTargetableForAttack() && pWho->isInAccessablePlaceFor(m_creature)) + { + // AssistPlayerInCombat can start attack, so return if true + if (HasFollowState(STATE_FOLLOW_INPROGRESS) && AssistPlayerInCombat(pWho)) + return; + + if (!m_creature->CanInitiateAttack()) + return; + + if (!m_creature->CanFly() && m_creature->GetDistanceZ(pWho) > CREATURE_Z_ATTACK_RANGE) + return; + + if (m_creature->IsHostileTo(pWho)) + { + float fAttackRadius = m_creature->GetAttackDistance(pWho); + if (m_creature->IsWithinDistInMap(pWho, fAttackRadius) && m_creature->IsWithinLOSInMap(pWho)) + { + if (!m_creature->getVictim()) + { + pWho->RemoveSpellsCausingAura(SPELL_AURA_MOD_STEALTH); + AttackStart(pWho); + } + else if (m_creature->GetMap()->IsDungeon()) + { + pWho->SetInCombatWith(m_creature); + m_creature->AddThreat(pWho); + } + } + } + } +} + +void CreatureFollowerAI::JustDied(Unit* /*pKiller*/) +{ + if (!HasFollowState(STATE_FOLLOW_INPROGRESS) || !m_leaderGuid || !m_pQuestForFollow) + return; + + // TODO: need a better check for quests with time limit. + if (Player* pPlayer = GetLeaderForFollower()) + { + if (Group* pGroup = pPlayer->GetGroup()) + { + for (GroupReference* pRef = pGroup->GetFirstMember(); pRef != NULL; pRef = pRef->next()) + { + if (Player* pMember = pRef->getSource()) + { + if (pMember->GetQuestStatus(m_pQuestForFollow->GetQuestId()) == QUEST_STATUS_INCOMPLETE) + pMember->FailQuest(m_pQuestForFollow->GetQuestId()); + } + } + } + else + { + if (pPlayer->GetQuestStatus(m_pQuestForFollow->GetQuestId()) == QUEST_STATUS_INCOMPLETE) + pPlayer->FailQuest(m_pQuestForFollow->GetQuestId()); + } + } +} + +void CreatureFollowerAI::JustRespawned() +{ + m_uiFollowState = STATE_FOLLOW_NONE; + + if (!IsCombatMovement()) + SetCombatMovement(true); + + Reset(); +} + +void CreatureFollowerAI::EnterEvadeMode() +{ + m_creature->RemoveAllAurasOnEvade(); + m_creature->DeleteThreatList(); + m_creature->CombatStop(true); + m_creature->SetLootRecipient(NULL); + + if (HasFollowState(STATE_FOLLOW_INPROGRESS)) + { + debug_log("SD2: CreatureFollowerAI left combat, returning to CombatStartPosition."); + + if (m_creature->GetMotionMaster()->GetCurrentMovementGeneratorType() == CHASE_MOTION_TYPE) + { + float fPosX, fPosY, fPosZ; + m_creature->GetCombatStartPosition(fPosX, fPosY, fPosZ); + m_creature->GetMotionMaster()->MovePoint(POINT_COMBAT_START, fPosX, fPosY, fPosZ); + } + } + else + { + if (m_creature->GetMotionMaster()->GetCurrentMovementGeneratorType() == CHASE_MOTION_TYPE) + m_creature->GetMotionMaster()->MoveTargetedHome(); + } + + Reset(); +} + +void CreatureFollowerAI::UpdateAI(const uint32 uiDiff) +{ + if (HasFollowState(STATE_FOLLOW_INPROGRESS) && !m_creature->getVictim()) + { + if (m_uiUpdateFollowTimer < uiDiff) + { + if (HasFollowState(STATE_FOLLOW_COMPLETE) && !HasFollowState(STATE_FOLLOW_POSTEVENT)) + { + debug_log("SD2: CreatureFollowerAI is set completed, despawns."); + m_creature->ForcedDespawn(); + return; + } + + bool bIsMaxRangeExceeded = true; + + if (Player* pPlayer = GetLeaderForFollower()) + { + if (HasFollowState(STATE_FOLLOW_RETURNING)) + { + debug_log("SD2: CreatureFollowerAI is returning to leader."); + + RemoveFollowState(STATE_FOLLOW_RETURNING); + m_creature->GetMotionMaster()->MoveFollow(pPlayer, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE); + return; + } + + if (Group* pGroup = pPlayer->GetGroup()) + { + for (GroupReference* pRef = pGroup->GetFirstMember(); pRef != NULL; pRef = pRef->next()) + { + Player* pMember = pRef->getSource(); + + if (pMember && m_creature->IsWithinDistInMap(pMember, MAX_PLAYER_DISTANCE)) + { + bIsMaxRangeExceeded = false; + break; + } + } + } + else + { + if (m_creature->IsWithinDistInMap(pPlayer, MAX_PLAYER_DISTANCE)) + bIsMaxRangeExceeded = false; + } + } + + if (bIsMaxRangeExceeded) + { + debug_log("SD2: CreatureFollowerAI failed because player/group was to far away or not found"); + m_creature->ForcedDespawn(); + return; + } + + m_uiUpdateFollowTimer = 1000; + } + else + m_uiUpdateFollowTimer -= uiDiff; + } + + UpdateFollowerAI(uiDiff); +} + +void CreatureFollowerAI::UpdateFollowerAI(const uint32 /*uiDiff*/) +{ + if (!m_creature->SelectHostileTarget() || !m_creature->getVictim()) + return; + + DoMeleeAttackIfReady(); +} + +void CreatureFollowerAI::MovementInform(uint32 uiMotionType, uint32 uiPointId) +{ + if (uiMotionType != POINT_MOTION_TYPE || !HasFollowState(STATE_FOLLOW_INPROGRESS)) + return; + + if (uiPointId == POINT_COMBAT_START) + { + if (GetLeaderForFollower()) + { + if (!HasFollowState(STATE_FOLLOW_PAUSED)) + AddFollowState(STATE_FOLLOW_RETURNING); + } + else + m_creature->ForcedDespawn(); + } +} + +void CreatureFollowerAI::StartFollow(Player* pLeader, uint32 uiFactionForFollower, const Quest* pQuest) +{ + if (m_creature->getVictim()) + { + debug_log("SD2: CreatureFollowerAI attempt to StartFollow while in combat."); + return; + } + + if (HasFollowState(STATE_FOLLOW_INPROGRESS)) + { + script_error_log("CreatureFollowerAI attempt to StartFollow while already following."); + return; + } + + // set variables + m_leaderGuid = pLeader->GetObjectGuid(); + + if (uiFactionForFollower) + m_creature->SetFactionTemporary(uiFactionForFollower, TEMPFACTION_RESTORE_RESPAWN); + + m_pQuestForFollow = pQuest; + + if (m_creature->GetMotionMaster()->GetCurrentMovementGeneratorType() == WAYPOINT_MOTION_TYPE) + { + m_creature->GetMotionMaster()->Clear(); + m_creature->GetMotionMaster()->MoveIdle(); + debug_log("SD2: CreatureFollowerAI start with WAYPOINT_MOTION_TYPE, set to MoveIdle."); + } + + m_creature->SetUInt32Value(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_NONE); + + AddFollowState(STATE_FOLLOW_INPROGRESS); + + m_creature->GetMotionMaster()->MoveFollow(pLeader, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE); + + debug_log("SD2: CreatureFollowerAI start follow %s (Guid %s)", pLeader->GetName(), m_leaderGuid.GetString().c_str()); +} + +Player* CreatureFollowerAI::GetLeaderForFollower() +{ + if (Player* pLeader = m_creature->GetMap()->GetPlayer(m_leaderGuid)) + { + if (pLeader->isAlive()) + return pLeader; + else + { + if (Group* pGroup = pLeader->GetGroup()) + { + for (GroupReference* pRef = pGroup->GetFirstMember(); pRef != NULL; pRef = pRef->next()) + { + Player* pMember = pRef->getSource(); + + if (pMember && pMember->isAlive() && m_creature->IsWithinDistInMap(pMember, MAX_PLAYER_DISTANCE)) + { + debug_log("SD2: CreatureFollowerAI GetLeader changed and returned new leader."); + m_leaderGuid = pMember->GetObjectGuid(); + return pMember; + } + } + } + } + } + + debug_log("SD2: CreatureFollowerAI GetLeader can not find suitable leader."); + return NULL; +} + +void CreatureFollowerAI::SetFollowComplete(bool bWithEndEvent) +{ + if (m_creature->GetMotionMaster()->GetCurrentMovementGeneratorType() == FOLLOW_MOTION_TYPE) + { + m_creature->StopMoving(); + m_creature->GetMotionMaster()->Clear(); + m_creature->GetMotionMaster()->MoveIdle(); + } + + if (bWithEndEvent) + AddFollowState(STATE_FOLLOW_POSTEVENT); + else + { + if (HasFollowState(STATE_FOLLOW_POSTEVENT)) + RemoveFollowState(STATE_FOLLOW_POSTEVENT); + } + + AddFollowState(STATE_FOLLOW_COMPLETE); +} + +void CreatureFollowerAI::SetFollowPaused(bool bPaused) +{ + if (!HasFollowState(STATE_FOLLOW_INPROGRESS) || HasFollowState(STATE_FOLLOW_COMPLETE)) + return; + + if (bPaused) + { + AddFollowState(STATE_FOLLOW_PAUSED); + + if (m_creature->GetMotionMaster()->GetCurrentMovementGeneratorType() == FOLLOW_MOTION_TYPE) + { + m_creature->StopMoving(); + m_creature->GetMotionMaster()->Clear(); + m_creature->GetMotionMaster()->MoveIdle(); + } + } + else + { + RemoveFollowState(STATE_FOLLOW_PAUSED); + + if (Player* pLeader = GetLeaderForFollower()) + m_creature->GetMotionMaster()->MoveFollow(pLeader, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE); + } +} diff --git a/src/game/CreatureFollowerAI.h b/src/game/CreatureFollowerAI.h new file mode 100644 index 0000000000..67c856358a --- /dev/null +++ b/src/game/CreatureFollowerAI.h @@ -0,0 +1,65 @@ +/* This file is part of the ScriptDev2 Project. See AUTHORS file for Copyright information + * This program is free software licensed under GPL version 2 + * Please see the included DOCS/LICENSE.TXT for more information */ + +#ifndef MANGOS_CREATURE_FOLLOWERAI_H +#define MANGOS_CREATURE_FOLLOWERAI_H + +enum FollowState +{ + 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 +}; + +class FollowerAI : public ScriptedAI +{ + public: + explicit FollowerAI(Creature* pCreature); + ~FollowerAI() {} + + // virtual void WaypointReached(uint32 uiPointId) = 0; + + void MovementInform(uint32 uiMotionType, uint32 uiPointId) override; + + void AttackStart(Unit*) override; + + void MoveInLineOfSight(Unit*) override; + + void EnterEvadeMode() override; + + void JustDied(Unit*) override; + + void JustRespawned() override; + + void UpdateAI(const uint32) override; // the "internal" update, calls UpdateFollowerAI() + virtual void UpdateFollowerAI(const uint32); // used when it's needed to add code in update (abilities, scripted events, etc) + + void StartFollow(Player* pPlayer, uint32 uiFactionForFollower = 0, const Quest* pQuest = NULL); + + void SetFollowPaused(bool bPaused); // if special event require follow mode to hold/resume during the follow + void SetFollowComplete(bool bWithEndEvent = false); + + bool HasFollowState(uint32 uiFollowState) { return (m_uiFollowState & uiFollowState); } + + protected: + Player* GetLeaderForFollower(); + + private: + void AddFollowState(uint32 uiFollowState) { m_uiFollowState |= uiFollowState; } + void RemoveFollowState(uint32 uiFollowState) { m_uiFollowState &= ~uiFollowState; } + + bool AssistPlayerInCombat(Unit* pWho); + + ObjectGuid m_leaderGuid; + uint32 m_uiUpdateFollowTimer; + uint32 m_uiFollowState; + + const Quest* m_pQuestForFollow; // normally we have a quest +}; + +#endif \ No newline at end of file