/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see .
*/
#include "ScriptedEscortAI.h"
#include "Group.h"
#include "Player.h"
#include "ScriptedCreature.h"
enum ePoints
{
POINT_LAST_POINT = 0xFFFFFF,
POINT_HOME = 0xFFFFFE
};
npc_escortAI::npc_escortAI(Creature* creature) : ScriptedAI(creature),
m_uiWPWaitTimer(1000),
m_uiPlayerCheckTimer(0),
m_uiEscortState(STATE_ESCORT_NONE),
MaxPlayerDistance(DEFAULT_MAX_PLAYER_DISTANCE),
m_pQuestForEscort(nullptr),
m_bIsActiveAttacker(true),
m_bCanInstantRespawn(false),
m_bCanReturnToStart(false),
DespawnAtEnd(true),
DespawnAtFar(true),
ScriptWP(false),
HasImmuneToNPCFlags(false)
{}
void npc_escortAI::AttackStart(Unit* who)
{
if (!who)
return;
if (me->Attack(who, true))
{
MovementGeneratorType type = me->GetMotionMaster()->GetMotionSlotType(MOTION_SLOT_ACTIVE);
if (type == ESCORT_MOTION_TYPE || type == POINT_MOTION_TYPE)
{
me->GetMotionMaster()->MovementExpired();
//me->DisableSpline();
me->StopMoving();
}
if (me->IsCombatMovementAllowed())
me->GetMotionMaster()->MoveChase(who);
}
}
//see followerAI
bool npc_escortAI::AssistPlayerInCombatAgainst(Unit* who)
{
if (!who || !who->GetVictim())
{
return false;
}
if (me->HasReactState(REACT_PASSIVE))
{
return false;
}
//experimental (unknown) flag not present
if (!(me->GetCreatureTemplate()->type_flags & CREATURE_TYPE_FLAG_CAN_ASSIST))
{
return false;
}
//not a player
if (!who->GetVictim()->GetCharmerOrOwnerPlayerOrPlayerItself())
{
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->IsCreature() && who->ToCreature()->IsInEvadeMode())
{
return false;
}
//never attack friendly
if (!me->IsValidAttackTarget(who))
{
return false;
}
//too far away and no free sight?
if (me->IsWithinDistInMap(who, GetMaxPlayerDistance()) && me->IsWithinLOSInMap(who))
{
AttackStart(who);
return true;
}
return false;
}
void npc_escortAI::MoveInLineOfSight(Unit* who)
{
if (me->GetVictim())
return;
if (!me->HasUnitState(UNIT_STATE_STUNNED) && who->isTargetableForAttack(true, me) && who->isInAccessiblePlaceFor(me))
if (HasEscortState(STATE_ESCORT_ESCORTING) && AssistPlayerInCombatAgainst(who))
return;
if (me->HasReactState(REACT_AGGRESSIVE) && me->CanStartAttack(who))
{
if (me->HasUnitState(UNIT_STATE_DISTRACTED))
{
me->ClearUnitState(UNIT_STATE_DISTRACTED);
me->GetMotionMaster()->Clear();
}
AttackStart(who);
}
}
void npc_escortAI::JustDied(Unit* /*killer*/)
{
if (!HasEscortState(STATE_ESCORT_ESCORTING) || !m_uiPlayerGUID || !m_pQuestForEscort)
return;
if (Player* player = GetPlayerForEscort())
{
if (Group* group = player->GetGroup())
{
for (GroupReference* groupRef = group->GetFirstMember(); groupRef != nullptr; groupRef = groupRef->next())
if (Player* member = groupRef->GetSource())
if (member->IsInMap(player) && member->GetQuestStatus(m_pQuestForEscort->GetQuestId()) == QUEST_STATUS_INCOMPLETE)
member->FailQuest(m_pQuestForEscort->GetQuestId());
}
else
{
if (player->GetQuestStatus(m_pQuestForEscort->GetQuestId()) == QUEST_STATUS_INCOMPLETE)
player->FailQuest(m_pQuestForEscort->GetQuestId());
}
}
}
void npc_escortAI::JustRespawned()
{
RemoveEscortState(STATE_ESCORT_ESCORTING | STATE_ESCORT_RETURNING | STATE_ESCORT_PAUSED);
if (!me->IsCombatMovementAllowed())
me->SetCombatMovement(true);
//add a small delay before going to first waypoint, normal in near all cases
m_uiWPWaitTimer = 1000;
if (me->GetFaction() != me->GetCreatureTemplate()->faction)
me->RestoreFaction();
Reset();
}
void npc_escortAI::ReturnToLastPoint()
{
float x, y, z, o;
me->GetHomePosition(x, y, z, o);
me->GetMotionMaster()->MovePoint(POINT_LAST_POINT, x, y, z, FORCED_MOVEMENT_RUN);
}
void npc_escortAI::EnterEvadeMode(EvadeReason /*why*/)
{
me->GetThreatMgr().ClearAllThreat();
me->CombatStop(true);
me->SetLootRecipient(nullptr);
if (HasEscortState(STATE_ESCORT_ESCORTING))
{
AddEscortState(STATE_ESCORT_RETURNING);
ReturnToLastPoint();
LOG_DEBUG("scripts.ai", "EscortAI has left combat and is now returning to last point");
}
else
{
me->GetMotionMaster()->MoveTargetedHome();
if (HasImmuneToNPCFlags)
me->SetImmuneToNPC(true);
Reset();
}
}
bool npc_escortAI::IsPlayerOrGroupInRange()
{
if (Player* player = GetPlayerForEscort())
{
if (Group* group = player->GetGroup())
{
for (GroupReference* groupRef = group->GetFirstMember(); groupRef != nullptr; groupRef = groupRef->next())
if (Player* member = groupRef->GetSource())
if (me->IsWithinDistInMap(member, GetMaxPlayerDistance(), true, false, false))
return true;
}
else if (me->IsWithinDistInMap(player, GetMaxPlayerDistance(), true, false, false))
return true;
}
return false;
}
void npc_escortAI::UpdateAI(uint32 diff)
{
if (HasEscortState(STATE_ESCORT_ESCORTING) && !me->GetVictim() && m_uiWPWaitTimer && !HasEscortState(STATE_ESCORT_RETURNING))
{
if (m_uiWPWaitTimer <= diff)
{
if (CurrentWP == WaypointList.end())
{
if (DespawnAtEnd)
{
if (m_bCanReturnToStart)
{
float fRetX, fRetY, fRetZ;
me->GetRespawnPosition(fRetX, fRetY, fRetZ);
me->GetMotionMaster()->MovePoint(POINT_HOME, fRetX, fRetY, fRetZ);
m_uiWPWaitTimer = 0;
return;
}
if (m_bCanInstantRespawn)
{
me->setDeathState(DeathState::JustDied);
me->Respawn();
}
else
me->DespawnOrUnsummon();
}
// xinef: remove escort state, escort was finished (lack of this line resulted in skipping UpdateEscortAI calls after finished escort)
RemoveEscortState(STATE_ESCORT_ESCORTING);
return;
}
if (!HasEscortState(STATE_ESCORT_PAUSED))
{
// xinef, start escort if there is no spline active
if (me->movespline->Finalized())
{
Movement::PointsArray pathPoints;
GenerateWaypointArray(&pathPoints);
me->GetMotionMaster()->MoveSplinePath(&pathPoints);
}
WaypointStart(CurrentWP->id);
m_uiWPWaitTimer = 0;
}
}
else
m_uiWPWaitTimer -= diff;
}
//Check if player or any member of his group is within range
if (HasEscortState(STATE_ESCORT_ESCORTING) && m_uiPlayerGUID && !me->GetVictim() && !HasEscortState(STATE_ESCORT_RETURNING))
{
m_uiPlayerCheckTimer += diff;
if (m_uiPlayerCheckTimer > 1000)
{
if (DespawnAtFar && !IsPlayerOrGroupInRange())
{
if (m_bCanInstantRespawn)
{
me->setDeathState(DeathState::JustDied);
me->Respawn();
}
else
me->DespawnOrUnsummon();
return;
}
m_uiPlayerCheckTimer = 0;
}
}
UpdateEscortAI(diff);
}
void npc_escortAI::UpdateEscortAI(uint32 /*diff*/)
{
if (!UpdateVictim())
return;
DoMeleeAttackIfReady();
}
void npc_escortAI::MovementInform(uint32 moveType, uint32 pointId)
{
// xinef: no action allowed if there is no escort
if (!HasEscortState(STATE_ESCORT_ESCORTING))
return;
if (moveType == POINT_MOTION_TYPE)
{
//Combat start position reached, continue waypoint movement
if (pointId == POINT_LAST_POINT)
{
LOG_DEBUG("scripts.ai", "EscortAI has returned to original position before combat");
RemoveEscortState(STATE_ESCORT_RETURNING);
if (!m_uiWPWaitTimer)
m_uiWPWaitTimer = 1;
}
else if (pointId == POINT_HOME)
{
LOG_DEBUG("scripts.ai", "EscortAI has returned to original home location and will continue from beginning of waypoint list.");
CurrentWP = WaypointList.begin();
m_uiWPWaitTimer = 1;
}
}
else if (moveType == ESCORT_MOTION_TYPE)
{
if (m_uiWPWaitTimer <= 1 && !HasEscortState(STATE_ESCORT_PAUSED) && CurrentWP != WaypointList.end())
{
//Call WP function
me->SetPosition(CurrentWP->x, CurrentWP->y, CurrentWP->z, me->GetOrientation());
me->SetHomePosition(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ(), me->GetOrientation());
WaypointReached(CurrentWP->id);
m_uiWPWaitTimer = CurrentWP->WaitTimeMs + 1;
++CurrentWP;
if (m_uiWPWaitTimer > 1 || HasEscortState(STATE_ESCORT_PAUSED))
{
if (me->GetMotionMaster()->GetMotionSlotType(MOTION_SLOT_ACTIVE) == ESCORT_MOTION_TYPE)
me->GetMotionMaster()->MovementExpired();
me->StopMovingOnCurrentPos();
me->GetMotionMaster()->MoveIdle();
}
}
}
}
/*
void npc_escortAI::OnPossess(bool apply)
{
// We got possessed in the middle of being escorted, store the point
// where we left off to come back to when possess is removed
if (HasEscortState(STATE_ESCORT_ESCORTING))
{
if (apply)
me->GetPosition(LastPos.x, LastPos.y, LastPos.z);
else
{
Returning = true;
me->GetMotionMaster()->MovementExpired();
me->GetMotionMaster()->MovePoint(WP_LAST_POINT, LastPos.x, LastPos.y, LastPos.z);
}
}
}
*/
void npc_escortAI::AddWaypoint(uint32 id, float x, float y, float z, uint32 waitTime)
{
Escort_Waypoint t(id, x, y, z, waitTime);
WaypointList.push_back(t);
// i think SD2 no longer uses this function
ScriptWP = true;
/*PointMovement wp;
wp.m_uiCreatureEntry = me->GetEntry();
wp.m_uiPointId = id;
wp.m_fX = x;
wp.m_fY = y;
wp.m_fZ = z;
wp.m_uiWaitTime = WaitTimeMs;
PointMovementMap[wp.m_uiCreatureEntry].push_back(wp);*/
}
void npc_escortAI::FillPointMovementListForCreature()
{
ScriptPointVector const& movePoints = sScriptSystemMgr->GetPointMoveList(me->GetEntry());
if (movePoints.empty())
return;
ScriptPointVector::const_iterator itrEnd = movePoints.end();
for (ScriptPointVector::const_iterator itr = movePoints.begin(); itr != itrEnd; ++itr)
{
Escort_Waypoint point(itr->uiPointId, itr->fX, itr->fY, itr->fZ, itr->uiWaitTime);
WaypointList.push_back(point);
}
}
//TODO: get rid of this many variables passed in function.
void npc_escortAI::Start(bool isActiveAttacker /* = true*/, ObjectGuid playerGUID /* = ObjectGuid::Empty */, Quest const* quest /* = nullptr */, bool instantRespawn /* = false */, bool canLoopPath /* = false */, bool resetWaypoints /* = true */)
{
if (me->GetVictim())
{
LOG_ERROR("entities.unit.ai", "ERROR: EscortAI (script: {}, creature entry: {}) attempts to Start while in combat", me->GetScriptName(), me->GetEntry());
return;
}
if (HasEscortState(STATE_ESCORT_ESCORTING))
{
LOG_ERROR("entities.unit.ai", "EscortAI (script: {}, creature entry: {}) attempts to Start while already escorting", me->GetScriptName(), me->GetEntry());
return;
}
if (!ScriptWP && resetWaypoints) // sd2 never adds wp in script, but tc does
{
if (!WaypointList.empty())
WaypointList.clear();
FillPointMovementListForCreature();
}
if (WaypointList.empty())
{
LOG_ERROR("sql.sql", "EscortAI (script: {}, creature entry: {}) starts with 0 waypoints (possible missing entry in script_waypoint. Quest: {}).",
me->GetScriptName(), me->GetEntry(), quest ? quest->GetQuestId() : 0);
return;
}
//set variables
m_bIsActiveAttacker = isActiveAttacker;
m_uiPlayerGUID = playerGUID;
m_pQuestForEscort = quest;
m_bCanInstantRespawn = instantRespawn;
m_bCanReturnToStart = canLoopPath;
if (m_bCanReturnToStart && m_bCanInstantRespawn)
LOG_DEBUG("scripts.ai", "EscortAI is set to return home after waypoint end and instant respawn at waypoint end. Creature will never despawn.");
if (me->GetMotionMaster()->GetCurrentMovementGeneratorType() == WAYPOINT_MOTION_TYPE)
{
me->GetMotionMaster()->MovementExpired();
me->GetMotionMaster()->MoveIdle();
LOG_DEBUG("scripts.ai", "EscortAI start with WAYPOINT_MOTION_TYPE, changed to MoveIdle.");
}
//disable npcflags
me->ReplaceAllNpcFlags(UNIT_NPC_FLAG_NONE);
if (me->IsImmuneToNPC())
{
HasImmuneToNPCFlags = true;
me->SetImmuneToNPC(false);
}
LOG_DEBUG("scripts.ai", "EscortAI started with {} waypoints. ActiveAttacker = {}, PlayerGUID = {}",
uint64(WaypointList.size()), m_bIsActiveAttacker, m_uiPlayerGUID.ToString());
CurrentWP = WaypointList.begin();
AddEscortState(STATE_ESCORT_ESCORTING);
if (me->GetMotionMaster()->GetMotionSlotType(MOTION_SLOT_ACTIVE) == ESCORT_MOTION_TYPE)
me->GetMotionMaster()->MovementExpired();
me->DisableSpline();
me->GetMotionMaster()->MoveIdle();
}
void npc_escortAI::SetEscortPaused(bool on)
{
if (!HasEscortState(STATE_ESCORT_ESCORTING))
return;
if (on)
AddEscortState(STATE_ESCORT_PAUSED);
else
RemoveEscortState(STATE_ESCORT_PAUSED);
}
bool npc_escortAI::SetNextWaypoint(uint32 pointId, float x, float y, float z, float orientation)
{
me->UpdatePosition(x, y, z, orientation);
return SetNextWaypoint(pointId, false);
}
bool npc_escortAI::SetNextWaypoint(uint32 pointId, bool setPosition)
{
if (!WaypointList.empty())
WaypointList.clear();
FillPointMovementListForCreature();
if (WaypointList.empty())
return false;
Escort_Waypoint waypoint(0, 0, 0, 0, 0);
for (CurrentWP = WaypointList.begin(); CurrentWP != WaypointList.end(); ++CurrentWP)
{
if (CurrentWP->id == pointId)
{
if (setPosition)
me->UpdatePosition(CurrentWP->x, CurrentWP->y, CurrentWP->z, me->GetOrientation());
return true;
}
}
return false;
}
bool npc_escortAI::GetWaypointPosition(uint32 pointId, float& x, float& y, float& z)
{
ScriptPointVector const& waypoints = sScriptSystemMgr->GetPointMoveList(me->GetEntry());
if (waypoints.empty())
return false;
for (ScriptPointVector::const_iterator itr = waypoints.begin(); itr != waypoints.end(); ++itr)
{
if (itr->uiPointId == pointId)
{
x = itr->fX;
y = itr->fY;
z = itr->fZ;
return true;
}
}
return false;
}
void npc_escortAI::GenerateWaypointArray(Movement::PointsArray* points)
{
if (WaypointList.empty())
return;
uint32 startingWaypointId = CurrentWP->id;
// Flying unit, just fill array
if (me->m_movementInfo.HasMovementFlag((MovementFlags)(MOVEMENTFLAG_CAN_FLY | MOVEMENTFLAG_DISABLE_GRAVITY)))
{
// xinef: first point in vector is unit real position
points->clear();
points->push_back(G3D::Vector3(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ()));
for (std::list::const_iterator itr = CurrentWP; itr != WaypointList.end(); ++itr)
points->push_back(G3D::Vector3(itr->x, itr->y, itr->z));
}
else
{
for (float size = 1.0f; size; size *= 0.5f)
{
std::vector pVector;
// xinef: first point in vector is unit real position
pVector.push_back(G3D::Vector3(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ()));
uint32 length = (WaypointList.size() - startingWaypointId) * size;
uint32 cnt = 0;
for (std::list::const_iterator itr = CurrentWP; itr != WaypointList.end() && cnt <= length; ++itr, ++cnt)
pVector.push_back(G3D::Vector3(itr->x, itr->y, itr->z));
if (pVector.size() > 2) // more than source + dest
{
G3D::Vector3 middle = (pVector[0] + pVector[pVector.size() - 1]) / 2.f;
G3D::Vector3 offset;
bool continueLoop = false;
for (uint32 i = 1; i < pVector.size() - 1; ++i)
{
offset = middle - pVector[i];
if (std::fabs(offset.x) >= 0xFF || std::fabs(offset.y) >= 0xFF || std::fabs(offset.z) >= 0x7F)
{
// offset is too big, split points
continueLoop = true;
break;
}
}
if (continueLoop)
continue;
}
// everything ok
*points = pVector;
break;
}
}
}