/* * 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 "Language.h" #include "Log.h" #include "Map.h" #include "MapReference.h" #include "MotionMaster.h" #include "Player.h" #include "SpellMgr.h" #include "TemporarySummon.h" #include "Vehicle.h" #include "World.h" //Disable CreatureAI when charmed void CreatureAI::OnCharmed(bool apply) { if (apply) { me->NeedChangeAI = true; me->IsAIEnabled = false; } } std::unordered_map, AISpellInfoType> UnitAI::AISpellInfo; AISpellInfoType* GetAISpellInfo(uint32 spellId, Difficulty difficulty) { return Trinity::Containers::MapGetValuePtr(UnitAI::AISpellInfo, { spellId, difficulty }); } CreatureAI::CreatureAI(Creature* creature) : UnitAI(creature), me(creature), _boundary(nullptr), _negateBoundary(false), m_MoveInLineOfSight_locked(false) { } CreatureAI::~CreatureAI() { } void CreatureAI::Talk(uint8 id, WorldObject const* whisperTarget /*= nullptr*/) { sCreatureTextMgr->SendChat(me, id, whisperTarget); } void CreatureAI::DoZoneInCombat(Creature* creature /*= nullptr*/, float maxRangeToNearestTarget /* = 250.0f*/) { if (!creature) creature = me; Map* map = creature->GetMap(); if (creature->CanHaveThreatList()) { if (!map->IsDungeon()) //use IsDungeon instead of Instanceable, in case battlegrounds will be instantiated { TC_LOG_ERROR("misc", "DoZoneInCombat call for map that isn't an instance (creature entry = %d)", creature->GetTypeId() == TYPEID_UNIT ? creature->ToCreature()->GetEntry() : 0); return; } if (!creature->HasReactState(REACT_PASSIVE) && !creature->GetVictim()) { if (Unit* nearTarget = creature->SelectNearestTarget(maxRangeToNearestTarget)) creature->AI()->AttackStart(nearTarget); else if (creature->IsSummon()) { if (Unit* summoner = creature->ToTempSummon()->GetSummoner()) { if (creature->IsFriendlyTo(summoner)) { Unit* target = summoner->getAttackerForHelper(); if (target && creature->IsHostileTo(target)) creature->AI()->AttackStart(target); } } } } // Intended duplicated check, the code above this should select a victim // If it can't find a suitable attack target then we should error out. if (!creature->HasReactState(REACT_PASSIVE) && !creature->GetVictim()) { TC_LOG_ERROR("misc.dozoneincombat", "DoZoneInCombat called for creature that has empty threat list (creature entry = %u)", creature->GetEntry()); return; } } Map::PlayerList const& playerList = map->GetPlayers(); if (playerList.isEmpty()) return; for (Map::PlayerList::const_iterator itr = playerList.begin(); itr != playerList.end(); ++itr) if (Player* player = itr->GetSource()) if (player->IsAlive()) creature->EngageWithTarget(player); } // 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 (m_MoveInLineOfSight_locked == true) return; m_MoveInLineOfSight_locked = true; MoveInLineOfSight(who); m_MoveInLineOfSight_locked = 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->StopMoving(); me->SetFacingTo(me->GetAngle(who)); } void CreatureAI::EnterEvadeMode(EvadeReason why) { if (!_EnterEvadeMode(why)) return; TC_LOG_DEBUG("entities.unit", "Creature %u enters evade mode.", me->GetEntry()); if (!me->GetVehicle()) // otherwise me will be in evade mode forever { if (Unit* owner = me->GetCharmerOrOwner()) { me->GetMotionMaster()->Clear(false); me->GetMotionMaster()->MoveFollow(owner, PET_FOLLOW_DIST, me->GetFollowAngle(), MOTION_SLOT_ACTIVE); } 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(); if (me->IsVehicle()) // use the same sequence of addtoworld, aireset may remove all summons! me->GetVehicleKit()->Reset(true); } void CreatureAI::SetGazeOn(Unit* target) { if (me->IsValidAttackTarget(target)) { if (!me->IsFocusing(nullptr, true) && target != me->GetVictim()) AttackStart(target); me->SetReactState(REACT_PASSIVE); } } bool CreatureAI::UpdateVictimWithGaze() { if (!me->IsEngaged()) return false; if (me->HasReactState(REACT_PASSIVE)) { if (me->GetVictim()) return true; else me->SetReactState(REACT_AGGRESSIVE); } if (Unit* victim = me->SelectVictim()) if (!me->IsFocusing(nullptr, true) && victim != me->GetVictim()) AttackStart(victim); return me->GetVictim() != nullptr; } bool CreatureAI::UpdateVictim() { if (!me->IsEngaged()) return false; if (!me->HasReactState(REACT_PASSIVE)) { if (Unit* victim = me->SelectVictim()) if (!me->IsFocusing(nullptr, true) && 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; } bool CreatureAI::_EnterEvadeMode(EvadeReason /*why*/) { if (!me->IsAlive()) return false; me->RemoveAurasOnEvade(); // sometimes bosses stuck in combat? me->GetThreatManager().ClearAllThreat(); me->CombatStop(true); me->SetLootRecipient(nullptr); me->ResetPlayerDamageReq(); me->SetLastDamagedTime(0); me->SetCannotReachTarget(false); me->DoNotReacquireTarget(); if (me->IsInEvadeMode()) return false; 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(uint32 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 (!CheckBoundary(&startPosition)) { // fall back to creature position startPosition = me->GetPosition(); if (!CheckBoundary(&startPosition)) { // fall back to creature home position startPosition = me->GetHomePosition(); if (!CheckBoundary(&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 off : std::initializer_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 (CheckBoundary(&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 * IN_MILLISECONDS)) { point->SetObjectScale(BOUNDARY_VISUALIZE_CREATURE_SCALE); point->AddUnitFlag(UNIT_FLAG_STUNNED); point->SetImmuneToAll(true); if (!hasOutOfBoundsNeighbor) point->AddUnitFlag(UNIT_FLAG_NOT_SELECTABLE); } Q.pop(); } return boundsWarning ? LANG_CREATURE_MOVEMENT_MAYBE_UNBOUNDED : 0; } bool CreatureAI::CheckBoundary(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 (CheckBoundary()) return true; else { EnterEvadeMode(EVADE_REASON_BOUNDARY); return false; } } Creature* CreatureAI::DoSummon(uint32 entry, Position const& pos, uint32 despawnTime, TempSummonType summonType) { return me->SummonCreature(entry, pos, summonType, despawnTime); } Creature* CreatureAI::DoSummon(uint32 entry, WorldObject* obj, float radius, uint32 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, uint32 despawnTime, TempSummonType summonType) { Position pos = obj->GetRandomNearPosition(radius); pos.m_positionZ += flightZ; return me->SummonCreature(entry, pos, summonType, despawnTime); }