/* * This file is part of the TrinityCore 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 "Creature.h" #include "Group.h" #include "Log.h" #include "Map.h" #include "MotionMaster.h" #include "MovementGenerator.h" #include "ObjectAccessor.h" #include "Player.h" #include "ScriptSystem.h" #include "WaypointManager.h" #include "World.h" enum Points { POINT_LAST_POINT = 0xFFFFFF, POINT_HOME = 0xFFFFFE }; EscortAI::EscortAI(Creature* creature) noexcept : ScriptedAI(creature), _pauseTimer(2500ms), _playerCheckTimer(1000), _escortState(STATE_ESCORT_NONE), _maxPlayerDistance(DEFAULT_MAX_PLAYER_DISTANCE), _escortQuest(nullptr), _activeAttacker(true), _instantRespawn(false), _returnToStart(false), _despawnAtEnd(true), _despawnAtFar(true), _hasImmuneToNPCFlags(false), _started(false), _ended(false), _resume(false) { } void EscortAI::MoveInLineOfSight(Unit* who) { if (!who) return; if (HasEscortState(STATE_ESCORT_ESCORTING) && AssistPlayerInCombatAgainst(who)) return; ScriptedAI::MoveInLineOfSight(who); } void EscortAI::JustDied(Unit* /*killer*/) { if (!HasEscortState(STATE_ESCORT_ESCORTING) || !_playerGUID || !_escortQuest) return; if (Player* player = GetPlayerForEscort()) { if (Group* group = player->GetGroup()) { for (GroupReference const& groupRef : group->GetMembers()) if (groupRef.GetSource()->IsInMap(player)) groupRef.GetSource()->FailQuest(_escortQuest->GetQuestId()); } else player->FailQuest(_escortQuest->GetQuestId()); } } void EscortAI::InitializeAI() { _escortState = STATE_ESCORT_NONE; if (!IsCombatMovementAllowed()) SetCombatMovement(true); // add a small delay before going to first waypoint, normal in near all cases _pauseTimer = 2s; if (me->GetFaction() != me->GetCreatureTemplate()->faction) me->RestoreFaction(); Reset(); } void EscortAI::ReturnToLastPoint() { me->GetMotionMaster()->MovePoint(POINT_LAST_POINT, me->GetHomePosition()); } void EscortAI::EnterEvadeMode(EvadeReason /*why*/) { me->RemoveAllAuras(); me->CombatStop(true); if (!me->IsTapListNotClearedOnEvade()) me->SetTappedBy(nullptr); EngagementOver(); if (HasEscortState(STATE_ESCORT_ESCORTING)) { AddEscortState(STATE_ESCORT_RETURNING); ReturnToLastPoint(); TC_LOG_DEBUG("scripts.ai.escortai", "EscortAI::EnterEvadeMode: left combat and is now returning to last point ({})", me->GetGUID().ToString()); } else { me->GetMotionMaster()->MoveTargetedHome(); if (_hasImmuneToNPCFlags) me->SetImmuneToNPC(true); Reset(); } } void EscortAI::MovementInform(uint32 type, uint32 id) { // no action allowed if there is no escort if (!HasEscortState(STATE_ESCORT_ESCORTING)) return; if (type == POINT_MOTION_TYPE) { if (_pauseTimer == 0s) _pauseTimer = 2s; // continue waypoint movement if (id == POINT_LAST_POINT) { TC_LOG_DEBUG("scripts.ai.escortai", "EscortAI::MovementInform: returned to before combat position ({})", me->GetGUID().ToString()); me->SetWalk(false); RemoveEscortState(STATE_ESCORT_RETURNING); } else if (id == POINT_HOME) { TC_LOG_DEBUG("scripts.ai.escortai", "EscortAI::MovementInform: returned to home location and restarting waypoint path ({})", me->GetGUID().ToString()); _started = false; } } else if (type == WAYPOINT_MOTION_TYPE) { ASSERT(id < _path.Nodes.size(), "EscortAI::MovementInform: referenced movement id (%u) points to non-existing node in loaded path (%s)", id, me->GetGUID().ToString().c_str()); WaypointNode waypoint = _path.Nodes[id]; TC_LOG_DEBUG("scripts.ai.escortai", "EscortAI::MovementInform: waypoint node {} reached ({})", waypoint.Id, me->GetGUID().ToString()); // last point if (id == _path.Nodes.size() - 1) { _started = false; _ended = true; _pauseTimer = 1s; } } } void EscortAI::UpdateAI(uint32 diff) { // Waypoint Updating if (HasEscortState(STATE_ESCORT_ESCORTING) && !me->IsEngaged() && !HasEscortState(STATE_ESCORT_RETURNING)) { if (_pauseTimer.count() <= diff) { if (!HasEscortState(STATE_ESCORT_PAUSED)) { _pauseTimer = 0s; if (_ended) { _ended = false; me->GetMotionMaster()->MoveIdle(); if (_despawnAtEnd) { TC_LOG_DEBUG("scripts.ai.escortai", "EscortAI::UpdateAI: reached end of waypoints, despawning at end ({})", me->GetGUID().ToString()); if (_returnToStart) { Position respawnPosition; float orientation = 0.f; me->GetRespawnPosition(respawnPosition.m_positionX, respawnPosition.m_positionY, respawnPosition.m_positionZ, &orientation); respawnPosition.SetOrientation(orientation); me->GetMotionMaster()->MovePoint(POINT_HOME, respawnPosition); TC_LOG_DEBUG("scripts.ai.escortai", "EscortAI::UpdateAI: returning to spawn location: {} ({})", respawnPosition.ToString(), me->GetGUID().ToString()); } else if (_instantRespawn) me->Respawn(true); else me->DespawnOrUnsummon(); } TC_LOG_DEBUG("scripts.ai.escortai", "EscortAI::UpdateAI: reached end of waypoints ({})", me->GetGUID().ToString()); RemoveEscortState(STATE_ESCORT_ESCORTING); return; } if (!_started) { _started = true; me->GetMotionMaster()->MovePath(_path, false); } else if (_resume) { _resume = false; if (MovementGenerator* movementGenerator = me->GetMotionMaster()->GetCurrentMovementGenerator(MOTION_SLOT_DEFAULT)) movementGenerator->Resume(0); } } } else _pauseTimer -= Milliseconds(diff); } // Check if player or any member of his group is within range if (_despawnAtFar && HasEscortState(STATE_ESCORT_ESCORTING) && !_playerGUID.IsEmpty() && !me->IsEngaged() && !HasEscortState(STATE_ESCORT_RETURNING)) { if (_playerCheckTimer <= diff) { if (!IsPlayerOrGroupInRange()) { TC_LOG_DEBUG("scripts.ai.escortai", "EscortAI::UpdateAI: failed because player/group was to far away or not found ({})", me->GetGUID().ToString()); bool isEscort = false; if (CreatureData const* creatureData = me->GetCreatureData()) isEscort = (sWorld->getBoolConfig(CONFIG_RESPAWN_DYNAMIC_ESCORTNPC) && (creatureData->spawnGroupData->flags & SPAWNGROUP_FLAG_ESCORTQUESTNPC)); if (_instantRespawn) { if (!isEscort) me->DespawnOrUnsummon(0s, 1s); else me->GetMap()->Respawn(SPAWN_TYPE_CREATURE, me->GetSpawnId()); } else me->DespawnOrUnsummon(); return; } _playerCheckTimer = 1000; } else _playerCheckTimer -= diff; } UpdateEscortAI(diff); } void EscortAI::UpdateEscortAI(uint32 /*diff*/) { UpdateVictim(); } void EscortAI::AddWaypoint(uint32 id, float x, float y, float z, bool run) { AddWaypoint(id, x, y, z, 0.0f, {}, run); } void EscortAI::AddWaypoint(uint32 id, float x, float y, float z, float orientation/* = 0*/, Optional waitTime/* = {}*/, bool run /*= false*/) { Trinity::NormalizeMapCoord(x); Trinity::NormalizeMapCoord(y); WaypointNode& waypoint = _path.Nodes.emplace_back(id, x, y, z, orientation, waitTime); waypoint.MoveType = run ? WaypointMoveType::Run : WaypointMoveType::Walk; } void EscortAI::ResetPath() { _path.Nodes.clear(); } void EscortAI::LoadPath(uint32 pathId) { WaypointPath const* path = sWaypointMgr->GetPath(pathId); if (!path) { TC_LOG_ERROR("scripts.ai.escortai", "EscortAI::LoadPath: (script: {}) path {} is invalid ({})", me->GetScriptName(), pathId, me->GetGUID().ToString()); return; } _path = *path; } /// @todo get rid of this many variables passed in function. void EscortAI::Start(bool isActiveAttacker /* = true*/, ObjectGuid playerGUID /* = 0 */, Quest const* quest /* = nullptr */, bool instantRespawn /* = false */, bool canLoopPath /* = false */) { if (_path.Nodes.empty()) { TC_LOG_ERROR("scripts.ai.escortai", "EscortAI::Start: (script: {}) path is empty ({})", me->GetScriptName(), me->GetGUID().ToString()); return; } // Queue respawn from the point it starts if (CreatureData const* cdata = me->GetCreatureData()) { if (sWorld->getBoolConfig(CONFIG_RESPAWN_DYNAMIC_ESCORTNPC) && (cdata->spawnGroupData->flags & SPAWNGROUP_FLAG_ESCORTQUESTNPC)) me->SaveRespawnTime(me->GetRespawnDelay()); } if (me->IsEngaged()) { TC_LOG_ERROR("scripts.ai.escortai", "EscortAI::Start: (script: {}) attempts to Start while in combat ({})", me->GetScriptName(), me->GetGUID().ToString()); return; } if (HasEscortState(STATE_ESCORT_ESCORTING)) { TC_LOG_ERROR("scripts.ai.escortai", "EscortAI::Start: (script: {}) attempts to Start while already escorting ({})", me->GetScriptName(), me->GetGUID().ToString()); return; } if (_path.Nodes.empty()) { TC_LOG_ERROR("scripts.ai.escortai", "EscortAI::Start: (script: {}) is set to return home after waypoint end and instant respawn at waypoint end. Creature will never despawn ({})", me->GetScriptName(), me->GetGUID().ToString()); return; } // set variables _activeAttacker = isActiveAttacker; _playerGUID = playerGUID; _escortQuest = quest; _instantRespawn = instantRespawn; _returnToStart = canLoopPath; if (_returnToStart && _instantRespawn) TC_LOG_ERROR("scripts.ai.escortai", "EscortAI::Start: (script: {}) is set to return home after waypoint end and instant respawn at waypoint end. Creature will never despawn ({})", me->GetScriptName(), me->GetGUID().ToString()); me->GetMotionMaster()->MoveIdle(); me->GetMotionMaster()->Clear(MOTION_PRIORITY_NORMAL); // disable npcflags me->ReplaceAllNpcFlags(UNIT_NPC_FLAG_NONE); me->ReplaceAllNpcFlags2(UNIT_NPC_FLAG_2_NONE); if (me->IsImmuneToNPC()) { _hasImmuneToNPCFlags = true; me->SetImmuneToNPC(false); } TC_LOG_DEBUG("scripts.ai.escortai", "EscortAI::Start: (script: {}) started with {} waypoints. ActiveAttacker = {}, Player = {} ({})", me->GetScriptName(), uint32(_path.Nodes.size()), _activeAttacker, _playerGUID.ToString(), me->GetGUID().ToString()); _started = false; AddEscortState(STATE_ESCORT_ESCORTING); } void EscortAI::SetEscortPaused(bool on) { if (!HasEscortState(STATE_ESCORT_ESCORTING)) return; if (on) { AddEscortState(STATE_ESCORT_PAUSED); if (MovementGenerator* movementGenerator = me->GetMotionMaster()->GetCurrentMovementGenerator(MOTION_SLOT_DEFAULT)) movementGenerator->Pause(0); } else { RemoveEscortState(STATE_ESCORT_PAUSED); _resume = true; } } Player* EscortAI::GetPlayerForEscort() { return ObjectAccessor::GetPlayer(*me, _playerGUID); } // see followerAI bool EscortAI::AssistPlayerInCombatAgainst(Unit* who) { if (!who || !who->GetVictim()) return false; if (me->HasReactState(REACT_PASSIVE)) return false; // experimental (unknown) flag not present if (!(me->GetCreatureDifficulty()->TypeFlags & CREATURE_TYPE_FLAG_CAN_ASSIST)) return false; // not a player if (!who->EnsureVictim()->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->GetTypeId() == TYPEID_UNIT && who->ToCreature()->IsInEvadeMode()) return false; if (!me->IsValidAssistTarget(who->GetVictim())) return false; // too far away and no free sight if (me->IsWithinDistInMap(who, GetMaxPlayerDistance()) && me->IsWithinLOSInMap(who)) { me->EngageWithTarget(who); return true; } return false; } bool EscortAI::IsPlayerOrGroupInRange() { if (Player* player = GetPlayerForEscort()) { if (Group* group = player->GetGroup()) { for (GroupReference const& groupRef : group->GetMembers()) if (me->IsWithinDistInMap(groupRef.GetSource(), GetMaxPlayerDistance())) return true; } else if (me->IsWithinDistInMap(player, GetMaxPlayerDistance())) return true; } return false; }