/* * Copyright (C) 2008-2017 TrinityCore * Copyright (C) 2005-2009 MaNGOS * * 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 "Creature.h" #include "CreatureAIImpl.h" #include "CreatureTextMgr.h" #include "Language.h" #include "Log.h" #include "MapReference.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; } } AISpellInfoType* UnitAI::AISpellInfo; AISpellInfoType* GetAISpellInfo(uint32 i) { return &UnitAI::AISpellInfo[i]; } CreatureAI::CreatureAI(Creature* creature) : UnitAI(creature), me(creature), _boundary(nullptr), 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; if (!creature->CanHaveThreatList()) return; Map* map = creature->GetMap(); 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()) { Unit* target = summoner->getAttackerForHelper(); if (!target && summoner->CanHaveThreatList() && !summoner->getThreatManager().isThreatListEmpty()) target = summoner->getThreatManager().getHostilTarget(); if (target && (creature->IsFriendlyTo(summoner) || 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->IsGameMaster()) continue; if (player->IsAlive()) { creature->SetInCombatWith(player); player->SetInCombatWith(creature); creature->AddThreat(player, 0.0f); } /* Causes certain things to never leave the threat list (Priest Lightwell, etc): for (Unit::ControlList::const_iterator itr = player->m_Controlled.begin(); itr != player->m_Controlled.end(); ++itr) { creature->SetInCombatWith(*itr); (*itr)->SetInCombatWith(creature); creature->AddThreat(*itr, 0.0f); }*/ } } } // 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->GetVictim()) return; if (me->GetCreatureType() == CREATURE_TYPE_NON_COMBAT_PET) // non-combat pets should just stand there and look good;) return; if (me->HasReactState(REACT_AGGRESSIVE) && me->CanStartAttack(who, false)) AttackStart(who); //else if (who->GetVictim() && me->IsFriendlyTo(who) // && me->IsWithinDistInMap(who, sWorld->getIntConfig(CONFIG_CREATURE_FAMILY_ASSISTANCE_RADIUS)) // && me->CanStartAttack(who->GetVictim(), true)) /// @todo if we use true, it will not attack it when it arrives // me->GetMotionMaster()->MoveChase(who->GetVictim()); } // 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 in combat, is confused, stunned or fleeing, do nothing if (me->GetTypeId() != TYPEID_UNIT || me->IsInCombat() || 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->SetFacingTo(me->GetAngle(who->GetPositionX(), who->GetPositionY()), true); me->StopMoving(); me->GetMotionMaster()->MoveDistract(5 * IN_MILLISECONDS); } 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::AttackedBy(Unit* attacker) { if (!me->GetVictim()) AttackStart(attacker); }*/ void CreatureAI::SetGazeOn(Unit* target) { if (me->IsValidAttackTarget(target)) { if (!me->IsFocusing(nullptr, true)) AttackStart(target); me->SetReactState(REACT_PASSIVE); } } bool CreatureAI::UpdateVictimWithGaze() { if (!me->IsInCombat()) 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)) AttackStart(victim); return me->GetVictim() != nullptr; } bool CreatureAI::UpdateVictim() { if (!me->IsInCombat()) return false; if (!me->HasReactState(REACT_PASSIVE)) { if (Unit* victim = me->SelectVictim()) if (!me->IsFocusing(nullptr, true)) AttackStart(victim); return me->GetVictim() != nullptr; } else if (me->getThreatManager().isThreatListEmpty()) { EnterEvadeMode(EVADE_REASON_NO_HOSTILES); return false; } return true; } bool CreatureAI::_EnterEvadeMode(EvadeReason /*why*/) { if (!me->IsAlive()) return false; me->RemoveAurasOnEvade(); // sometimes bosses stuck in combat? me->DeleteThreatList(); me->CombatStop(true); me->SetLootRecipient(nullptr); me->ResetPlayerDamageReq(); me->SetLastDamagedTime(0); me->SetCannotReachTarget(false); if (me->IsInEvadeMode()) return false; return true; } 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->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC | UNIT_FLAG_STUNNED | UNIT_FLAG_IMMUNE_TO_NPC); if (!hasOutOfBoundsNeighbor) point->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE); } Q.pop(); } return boundsWarning ? LANG_CREATURE_MOVEMENT_MAYBE_UNBOUNDED : 0; } bool CreatureAI::CheckBoundary(Position const* who) const { if (!who) who = me; if (_boundary) for (AreaBoundary const* boundary : *_boundary) if (!boundary->IsWithinBoundary(who)) return false; return true; } void CreatureAI::SetBoundary(CreatureBoundary const* boundary) { _boundary = boundary; me->DoImmediateBoundaryCheck(); } bool CreatureAI::CheckInRoom() { if (CheckBoundary()) return true; else { EnterEvadeMode(EVADE_REASON_BOUNDARY); return false; } } Creature* CreatureAI::DoSummon(uint32 entry, const Position& 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); }