/* * 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 "CreatureAI.h" #include "AreaBoundary.h" #include "Containers.h" #include "Creature.h" #include "CreatureAIImpl.h" #include "CreatureTextMgr.h" #include "DB2Structure.h" #include "Errors.h" #include "Language.h" #include "Log.h" #include "Map.h" #include "MapReference.h" #include "MotionMaster.h" #include "ObjectAccessor.h" #include "Player.h" #include "SpellHistory.h" #include "TemporarySummon.h" #include "Vehicle.h" #include std::unordered_map, AISpellInfoType> UnitAI::AISpellInfo; AISpellInfoType* GetAISpellInfo(uint32 spellId, Difficulty difficulty) { return Trinity::Containers::MapGetValuePtr(UnitAI::AISpellInfo, { spellId, difficulty }); } CreatureAI::CreatureAI(Creature* creature, uint32 scriptId) : UnitAI(creature), me(creature), _boundary(nullptr), _negateBoundary(false), _scriptId(scriptId ? scriptId : creature->GetScriptId()), _isEngaged(false), _moveInLOSLocked(false) { ASSERT(_scriptId, "A CreatureAI was initialized with an invalid scriptId!"); } CreatureAI::~CreatureAI() { } void CreatureAI::Talk(uint8 id, WorldObject const* whisperTarget /*= nullptr*/) { sCreatureTextMgr->SendChat(me, id, whisperTarget); } // Disable CreatureAI when charmed void CreatureAI::OnCharmed(bool isNew) { if (isNew && !me->IsCharmed() && !me->LastCharmerGUID.IsEmpty()) { if (!me->HasReactState(REACT_PASSIVE)) { if (Unit* lastCharmer = ObjectAccessor::GetUnit(*me, me->LastCharmerGUID)) me->EngageWithTarget(lastCharmer); } me->LastCharmerGUID.Clear(); if (!me->IsInCombat()) EnterEvadeMode(EVADE_REASON_NO_HOSTILES); } UnitAI::OnCharmed(isNew); } void CreatureAI::DoZoneInCombat(Creature* creature /*= nullptr*/) { if (!creature) creature = me; Map* map = creature->GetMap(); if (!map->IsDungeon()) // use IsDungeon instead of Instanceable, in case battlegrounds will be instantiated { TC_LOG_ERROR("scripts.ai", "CreatureAI::DoZoneInCombat: call for map that isn't an instance ({})", creature->GetGUID().ToString()); return; } if (!map->HavePlayers()) return; for (MapReference const& ref : map->GetPlayers()) { if (Player* player = ref.GetSource()) { if (!player->IsAlive() || !CombatManager::CanBeginCombat(creature, player)) continue; creature->EngageWithTarget(player); for (Unit* pet : player->m_Controlled) creature->EngageWithTarget(pet); if (Unit* vehicle = player->GetVehicleBase()) creature->EngageWithTarget(vehicle); } } } // scripts does not take care about MoveInLineOfSight loops // MoveInLineOfSight can be called inside another MoveInLineOfSight and cause stack overflow void CreatureAI::MoveInLineOfSight_Safe(Unit* who) { if (_moveInLOSLocked == true) return; _moveInLOSLocked = true; MoveInLineOfSight(who); _moveInLOSLocked = false; } void CreatureAI::MoveInLineOfSight(Unit* who) { if (me->IsEngaged()) return; if (me->HasReactState(REACT_AGGRESSIVE) && me->CanStartAttack(who, false)) me->EngageWithTarget(who); } void CreatureAI::OnOwnerCombatInteraction(Unit* target) { if (!target || !me->IsAlive()) return; if (!me->HasReactState(REACT_PASSIVE) && me->CanStartAttack(target, true)) me->EngageWithTarget(target); } // Distract creature, if player gets too close while stealthed/prowling void CreatureAI::TriggerAlert(Unit const* who) const { // If there's no target, or target isn't a player do nothing if (!who || who->GetTypeId() != TYPEID_PLAYER) return; // If this unit isn't an NPC, is already distracted, is fighting, is confused, stunned or fleeing, do nothing if (me->GetTypeId() != TYPEID_UNIT || me->IsEngaged() || me->HasUnitState(UNIT_STATE_CONFUSED | UNIT_STATE_STUNNED | UNIT_STATE_FLEEING | UNIT_STATE_DISTRACTED)) return; // Only alert for hostiles! if (me->IsCivilian() || me->HasReactState(REACT_PASSIVE) || !me->IsHostileTo(who) || !me->_IsTargetAcceptable(who)) return; // Send alert sound (if any) for this creature me->SendAIReaction(AI_REACTION_ALERT); // Face the unit (stealthed player) and set distracted state for 5 seconds me->GetMotionMaster()->MoveDistract(5 * IN_MILLISECONDS, me->GetAbsoluteAngle(who)); } // adapted from logic in Spell:EffectSummonType before commit 8499434 static bool ShouldFollowOnSpawn(SummonPropertiesEntry const* properties) { // Summons without SummonProperties are generally scripted summons that don't belong to any owner if (!properties) return false; switch (properties->Control) { case SUMMON_CATEGORY_PET: return true; case SUMMON_CATEGORY_WILD: case SUMMON_CATEGORY_ALLY: case SUMMON_CATEGORY_UNK: if (properties->GetFlags().HasFlag(SummonPropertiesFlags::JoinSummonerSpawnGroup)) return true; switch (SummonTitle(properties->Title)) { case SummonTitle::Pet: case SummonTitle::Guardian: case SummonTitle::Runeblade: case SummonTitle::Minion: case SummonTitle::Companion: return true; default: return false; } default: return false; } } void CreatureAI::JustAppeared() { if (!IsEngaged()) { if (TempSummon* summon = me->ToTempSummon()) { // Only apply this to specific types of summons if (!summon->GetVehicle() && ShouldFollowOnSpawn(summon->m_Properties) && summon->CanFollowOwner()) { if (Unit* owner = summon->GetCharmerOrOwner()) { summon->GetMotionMaster()->Clear(); summon->GetMotionMaster()->MoveFollow(owner, PET_FOLLOW_DIST, summon->GetFollowAngle()); } } } } } void CreatureAI::JustEnteredCombat(Unit* who) { if (!IsEngaged() && !me->CanHaveThreatList()) EngagementStart(who); } void CreatureAI::EnterEvadeMode(EvadeReason why) { if (!_EnterEvadeMode(why)) return; TC_LOG_DEBUG("scripts.ai", "CreatureAI::EnterEvadeMode: entering evade mode (why: {}) ({})", why, me->GetGUID().ToString()); if (!me->GetVehicle()) // otherwise me will be in evade mode forever { if (Unit* owner = me->GetCharmerOrOwner()) { me->GetMotionMaster()->Clear(); me->GetMotionMaster()->MoveFollow(owner, PET_FOLLOW_DIST, me->GetFollowAngle()); } else { // Required to prevent attacking creatures that are evading and cause them to reenter combat // Does not apply to MoveFollow me->AddUnitState(UNIT_STATE_EVADE); me->GetMotionMaster()->MoveTargetedHome(); } } Reset(); } bool CreatureAI::UpdateVictim() { if (!IsEngaged()) return false; if (!me->IsAlive()) { EngagementOver(); return false; } if (!me->HasReactState(REACT_PASSIVE)) { if (Unit* victim = me->SelectVictim()) if (victim != me->GetVictim()) AttackStart(victim); return me->GetVictim() != nullptr; } else if (!me->IsInCombat()) { EnterEvadeMode(EVADE_REASON_NO_HOSTILES); return false; } else if (me->GetVictim()) me->AttackStop(); return true; } void CreatureAI::EngagementStart(Unit* who) { if (_isEngaged) { TC_LOG_ERROR("scripts.ai", "CreatureAI::EngagementStart called even though creature is already engaged. Creature debug info:\n{}", me->GetDebugInfo()); return; } _isEngaged = true; me->AtEngage(who); } void CreatureAI::EngagementOver() { if (!_isEngaged) { TC_LOG_DEBUG("scripts.ai", "CreatureAI::EngagementOver called even though creature is not currently engaged. Creature debug info:\n{}", me->GetDebugInfo()); return; } _isEngaged = false; me->AtDisengage(); } bool CreatureAI::_EnterEvadeMode(EvadeReason /*why*/) { if (me->IsInEvadeMode()) return false; if (!me->IsAlive()) { EngagementOver(); return false; } me->RemoveAurasOnEvade(); me->CombatStop(true); me->SetTappedBy(nullptr); me->ResetPlayerDamageReq(); me->SetLastDamagedTime(0); me->SetCannotReachTarget(false); me->DoNotReacquireSpellFocusTarget(); me->SetTarget(ObjectGuid::Empty); me->GetSpellHistory()->ResetAllCooldowns(); EngagementOver(); return true; } Optional CreatureAI::GetDialogStatus(Player* /*player*/) { return {}; } const uint32 BOUNDARY_VISUALIZE_CREATURE = 15425; const float BOUNDARY_VISUALIZE_CREATURE_SCALE = 0.25f; const int8 BOUNDARY_VISUALIZE_STEP_SIZE = 1; const int32 BOUNDARY_VISUALIZE_FAILSAFE_LIMIT = 750; const float BOUNDARY_VISUALIZE_SPAWN_HEIGHT = 5.0f; int32 CreatureAI::VisualizeBoundary(Seconds duration, Unit* owner, bool fill) const { typedef std::pair coordinate; if (!owner) return -1; if (!_boundary || _boundary->empty()) return LANG_CREATURE_MOVEMENT_NOT_BOUNDED; std::queue Q; std::unordered_set alreadyChecked; std::unordered_set outOfBounds; Position startPosition = owner->GetPosition(); if (!IsInBoundary(&startPosition)) // fall back to creature position { startPosition = me->GetPosition(); if (!IsInBoundary(&startPosition)) // fall back to creature home position { startPosition = me->GetHomePosition(); if (!IsInBoundary(&startPosition)) return LANG_CREATURE_NO_INTERIOR_POINT_FOUND; } } float spawnZ = startPosition.GetPositionZ() + BOUNDARY_VISUALIZE_SPAWN_HEIGHT; bool boundsWarning = false; Q.push({ 0,0 }); while (!Q.empty()) { coordinate front = Q.front(); bool hasOutOfBoundsNeighbor = false; for (coordinate const& off : std::list{ {1, 0}, {0, 1}, {-1, 0}, {0, -1} }) { coordinate next(front.first + off.first, front.second + off.second); if (next.first > BOUNDARY_VISUALIZE_FAILSAFE_LIMIT || next.first < -BOUNDARY_VISUALIZE_FAILSAFE_LIMIT || next.second > BOUNDARY_VISUALIZE_FAILSAFE_LIMIT || next.second < -BOUNDARY_VISUALIZE_FAILSAFE_LIMIT) { boundsWarning = true; continue; } if (alreadyChecked.find(next) == alreadyChecked.end()) // never check a coordinate twice { Position nextPos(startPosition.GetPositionX() + next.first*BOUNDARY_VISUALIZE_STEP_SIZE, startPosition.GetPositionY() + next.second*BOUNDARY_VISUALIZE_STEP_SIZE, startPosition.GetPositionZ()); if (IsInBoundary(&nextPos)) Q.push(next); else { outOfBounds.insert(next); hasOutOfBoundsNeighbor = true; } alreadyChecked.insert(next); } else if (outOfBounds.find(next) != outOfBounds.end()) hasOutOfBoundsNeighbor = true; } if (fill || hasOutOfBoundsNeighbor) { if (TempSummon* point = owner->SummonCreature(BOUNDARY_VISUALIZE_CREATURE, Position(startPosition.GetPositionX() + front.first * BOUNDARY_VISUALIZE_STEP_SIZE, startPosition.GetPositionY() + front.second * BOUNDARY_VISUALIZE_STEP_SIZE, spawnZ), TEMPSUMMON_TIMED_DESPAWN, duration)) { point->SetObjectScale(BOUNDARY_VISUALIZE_CREATURE_SCALE); point->SetUnitFlag(UNIT_FLAG_STUNNED); point->SetImmuneToAll(true); if (!hasOutOfBoundsNeighbor) point->SetUnitFlag(UNIT_FLAG_UNINTERACTIBLE); } } Q.pop(); } return boundsWarning ? LANG_CREATURE_MOVEMENT_MAYBE_UNBOUNDED : 0; } bool CreatureAI::IsInBoundary(Position const* who) const { if (!_boundary) return true; if (!who) who = me; return CreatureAI::IsInBounds(*_boundary, who) != _negateBoundary; } bool CreatureAI::IsInBounds(CreatureBoundary const& boundary, Position const* pos) { for (AreaBoundary const* areaBoundary : boundary) if (!areaBoundary->IsWithinBoundary(pos)) return false; return true; } void CreatureAI::SetBoundary(CreatureBoundary const* boundary, bool negateBoundaries /*= false*/) { _boundary = boundary; _negateBoundary = negateBoundaries; me->DoImmediateBoundaryCheck(); } bool CreatureAI::CheckInRoom() { if (IsInBoundary()) return true; else { EnterEvadeMode(EVADE_REASON_BOUNDARY); return false; } } Creature* CreatureAI::DoSummon(uint32 entry, Position const& pos, Milliseconds despawnTime, TempSummonType summonType) { return me->SummonCreature(entry, pos, summonType, despawnTime); } Creature* CreatureAI::DoSummon(uint32 entry, WorldObject* obj, float radius, Milliseconds despawnTime, TempSummonType summonType) { Position pos = obj->GetRandomNearPosition(radius); return me->SummonCreature(entry, pos, summonType, despawnTime); } Creature* CreatureAI::DoSummonFlyer(uint32 entry, WorldObject* obj, float flightZ, float radius, Milliseconds despawnTime, TempSummonType summonType) { Position pos = obj->GetRandomNearPosition(radius); pos.m_positionZ += flightZ; return me->SummonCreature(entry, pos, summonType, despawnTime); }