/* * 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 "PetAI.h" #include "AIException.h" #include "CharmInfo.h" #include "Creature.h" #include "Errors.h" #include "Group.h" #include "Log.h" #include "Map.h" #include "MotionMaster.h" #include "ObjectAccessor.h" #include "Pet.h" #include "Player.h" #include "Spell.h" #include "SpellHistory.h" #include "SpellInfo.h" #include "SpellMgr.h" int32 PetAI::Permissible(Creature const* creature) { if (creature->HasUnitTypeMask(UNIT_MASK_CONTROLABLE_GUARDIAN)) { if (reinterpret_cast(creature)->GetOwner()->GetTypeId() == TYPEID_PLAYER) return PERMIT_BASE_PROACTIVE; return PERMIT_BASE_REACTIVE; } return PERMIT_BASE_NO; } PetAI::PetAI(Creature* creature, uint32 scriptId) : CreatureAI(creature, scriptId), _tracker(TIME_INTERVAL_LOOK) { if (!me->GetCharmInfo()) throw InvalidAIException("Creature doesn't have a valid charm info"); UpdateAllies(); } void PetAI::UpdateAI(uint32 diff) { if (!me->IsAlive() || !me->GetCharmInfo()) return; Unit* owner = me->GetCharmerOrOwner(); if (_updateAlliesTimer <= diff) // UpdateAllies self set update timer UpdateAllies(); else _updateAlliesTimer -= diff; if (me->GetVictim() && me->EnsureVictim()->IsAlive()) { // is only necessary to stop casting, the pet must not exit combat if (!me->GetCurrentSpell(CURRENT_CHANNELED_SPELL) && // ignore channeled spells (Pin, Seduction) me->EnsureVictim()->HasBreakableByDamageCrowdControlAura(me)) { me->InterruptNonMeleeSpells(false); return; } if (NeedToStop()) { TC_LOG_TRACE("scripts.ai.petai", "PetAI::UpdateAI: AI stopped attacking {}", me->GetGUID().ToString()); StopAttack(); return; } } else { if (me->HasReactState(REACT_AGGRESSIVE) || me->GetCharmInfo()->IsAtStay()) { // Every update we need to check targets only in certain cases // Aggressive - Allow auto select if owner or pet don't have a target // Stay - Only pick from pet or owner targets / attackers so targets won't run by // while chasing our owner. Don't do auto select. // All other cases (ie: defensive) - Targets are assigned by DamageTaken(), OwnerAttackedBy(), OwnerAttacked(), etc. Unit* nextTarget = SelectNextTarget(me->HasReactState(REACT_AGGRESSIVE)); if (nextTarget) AttackStart(nextTarget); else HandleReturnMovement(); } else HandleReturnMovement(); } // Autocast (cast only in combat or persistent spells in any state) if (!me->HasUnitState(UNIT_STATE_CASTING)) { TargetSpellList targetSpellStore; for (uint8 i = 0; i < me->GetPetAutoSpellSize(); ++i) { uint32 spellID = me->GetPetAutoSpellOnPos(i); if (!spellID) continue; SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellID, me->GetMap()->GetDifficultyID()); if (!spellInfo) continue; if (me->GetSpellHistory()->HasGlobalCooldown(spellInfo)) continue; // check spell cooldown if (!me->GetSpellHistory()->IsReady(spellInfo)) continue; if (spellInfo->IsPositive()) { if (spellInfo->CanBeUsedInCombat(me)) { // Check if we're in combat or commanded to attack if (!me->IsInCombat() && !me->GetCharmInfo()->IsCommandAttack()) continue; } Spell* spell = new Spell(me, spellInfo, TRIGGERED_NONE); bool spellUsed = false; // Some spells can target enemy or friendly (DK Ghoul's Leap) // Check for enemy first (pet then owner) Unit* target = me->getAttackerForHelper(); if (!target && owner) target = owner->getAttackerForHelper(); if (target) { if (CanAttack(target) && spell->CanAutoCast(target)) { targetSpellStore.push_back(std::make_pair(target, spell)); spellUsed = true; } } if (spellInfo->HasEffect(SPELL_EFFECT_JUMP_DEST)) { if (!spellUsed) delete spell; continue; // Pets must only jump to target } // No enemy, check friendly if (!spellUsed) { for (ObjectGuid target : _allySet) { Unit* ally = ObjectAccessor::GetUnit(*me, target); //only buff targets that are in combat, unless the spell can only be cast while out of combat if (!ally) continue; if (spell->CanAutoCast(ally)) { targetSpellStore.push_back(std::make_pair(ally, spell)); spellUsed = true; break; } } } // No valid targets at all if (!spellUsed) delete spell; } else if (me->GetVictim() && CanAttack(me->GetVictim()) && spellInfo->CanBeUsedInCombat(me)) { Spell* spell = new Spell(me, spellInfo, TRIGGERED_NONE); if (spell->CanAutoCast(me->GetVictim())) targetSpellStore.push_back(std::make_pair(me->GetVictim(), spell)); else delete spell; } } // found units to cast on to if (!targetSpellStore.empty()) { TargetSpellList::iterator it = targetSpellStore.begin(); std::advance(it, urand(0, targetSpellStore.size() - 1)); Spell* spell = (*it).second; Unit* target = (*it).first; targetSpellStore.erase(it); SpellCastTargets targets; targets.SetUnitTarget(target); spell->prepare(targets); } // deleted cached Spell objects for (std::pair const& unitspellpair : targetSpellStore) delete unitspellpair.second; } // Update speed as needed to prevent dropping too far behind and despawning me->UpdateSpeed(MOVE_RUN); me->UpdateSpeed(MOVE_WALK); me->UpdateSpeed(MOVE_FLIGHT); } void PetAI::KilledUnit(Unit* victim) { // Called from Unit::Kill() in case where pet or owner kills something // if owner killed this victim, pet may still be attacking something else if (me->GetVictim() && me->GetVictim() != victim) return; // Clear target just in case. May help problem where health / focus / mana // regen gets stuck. Also resets attack command. // Can't use StopAttack() because that activates movement handlers and ignores // next target selection me->AttackStop(); me->InterruptNonMeleeSpells(false); // Before returning to owner, see if there are more things to attack if (Unit* nextTarget = SelectNextTarget(false)) AttackStart(nextTarget); else HandleReturnMovement(); // Return } void PetAI::AttackStart(Unit* target) { // Overrides Unit::AttackStart to prevent pet from switching off its assigned target if (!target || target == me) return; if (me->GetVictim() && me->EnsureVictim()->IsAlive()) return; _AttackStart(target); } void PetAI::_AttackStart(Unit* target) { // Check all pet states to decide if we can attack this target if (!CanAttack(target)) return; // Only chase if not commanded to stay or if stay but commanded to attack DoAttack(target, (!me->GetCharmInfo()->HasCommandState(COMMAND_STAY) || me->GetCharmInfo()->IsCommandAttack())); } void PetAI::OwnerAttackedBy(Unit* attacker) { // Called when owner takes damage. This function helps keep pets from running off // simply due to owner gaining aggro. if (!attacker || !me->IsAlive()) return; // Passive pets don't do anything if (me->HasReactState(REACT_PASSIVE)) return; // Prevent pet from disengaging from current target if (me->GetVictim() && me->EnsureVictim()->IsAlive()) return; // Continue to evaluate and attack if necessary AttackStart(attacker); } void PetAI::OwnerAttacked(Unit* target) { // Called when owner attacks something. Allows defensive pets to know // that they need to assist // Target might be NULL if called from spell with invalid cast targets if (!target || !me->IsAlive()) return; // Passive pets don't do anything if (me->HasReactState(REACT_PASSIVE)) return; // Prevent pet from disengaging from current target if (me->GetVictim() && me->EnsureVictim()->IsAlive()) return; // Continue to evaluate and attack if necessary AttackStart(target); } Unit* PetAI::SelectNextTarget(bool allowAutoSelect) const { // Provides next target selection after current target death. // This function should only be called internally by the AI // Targets are not evaluated here for being valid targets, that is done in _CanAttack() // The parameter: allowAutoSelect lets us disable aggressive pet auto targeting for certain situations // Passive pets don't do next target selection if (me->HasReactState(REACT_PASSIVE)) return nullptr; // Check pet attackers first so we don't drag a bunch of targets to the owner if (Unit* myAttacker = me->getAttackerForHelper()) if (!myAttacker->HasBreakableByDamageCrowdControlAura()) return myAttacker; // Not sure why we wouldn't have an owner but just in case... if (!me->GetCharmerOrOwner()) return nullptr; // Check owner attackers if (Unit* ownerAttacker = me->GetCharmerOrOwner()->getAttackerForHelper()) if (!ownerAttacker->HasBreakableByDamageCrowdControlAura()) return ownerAttacker; // Check owner victim // 3.0.2 - Pets now start attacking their owners victim in defensive mode as soon as the hunter does if (Unit* ownerVictim = me->GetCharmerOrOwner()->GetVictim()) return ownerVictim; // Neither pet or owner had a target and aggressive pets can pick any target // To prevent aggressive pets from chain selecting targets and running off, we // only select a random target if certain conditions are met. if (me->HasReactState(REACT_AGGRESSIVE) && allowAutoSelect) { if (!me->GetCharmInfo()->IsReturning() || me->GetCharmInfo()->IsFollowing() || me->GetCharmInfo()->IsAtStay()) if (Unit* nearTarget = me->SelectNearestHostileUnitInAggroRange(true, true)) return nearTarget; } // Default - no valid targets return nullptr; } void PetAI::HandleReturnMovement() { // Handles moving the pet back to stay or owner // Prevent activating movement when under control of spells // such as "Eyes of the Beast" if (me->IsCharmed()) return; if (!me->GetCharmInfo()) { TC_LOG_WARN("scripts.ai.petai", "me->GetCharmInfo() is NULL in PetAI::HandleReturnMovement(). Debug info: {}", GetDebugInfo()); return; } if (me->GetCharmInfo()->HasCommandState(COMMAND_STAY)) { if (!me->GetCharmInfo()->IsAtStay() && !me->GetCharmInfo()->IsReturning()) { // Return to previous position where stay was clicked float x, y, z; me->GetCharmInfo()->GetStayPosition(x, y, z); ClearCharmInfoFlags(); me->GetCharmInfo()->SetIsReturning(true); if (me->HasUnitState(UNIT_STATE_CHASE)) me->GetMotionMaster()->Remove(CHASE_MOTION_TYPE); me->GetMotionMaster()->MovePoint(me->GetGUID().GetCounter(), x, y, z); } } else // COMMAND_FOLLOW { if (!me->GetCharmInfo()->IsFollowing() && !me->GetCharmInfo()->IsReturning()) { ClearCharmInfoFlags(); me->GetCharmInfo()->SetIsReturning(true); if (me->HasUnitState(UNIT_STATE_CHASE)) me->GetMotionMaster()->Remove(CHASE_MOTION_TYPE); me->GetMotionMaster()->MoveFollow(me->GetCharmerOrOwner(), PET_FOLLOW_DIST, me->GetFollowAngle()); } } me->RemoveUnitFlag(UNIT_FLAG_PET_IN_COMBAT); // on player pets, this flag indicates that we're actively going after a target - we're returning, so remove it } void PetAI::DoAttack(Unit* target, bool chase) { // Handles attack with or without chase and also resets flags // for next update / creature kill if (me->Attack(target, true)) { me->SetUnitFlag(UNIT_FLAG_PET_IN_COMBAT); // on player pets, this flag indicates we're actively going after a target - that's what we're doing, so set it // Play sound to let the player know the pet is attacking something it picked on its own if (me->HasReactState(REACT_AGGRESSIVE) && !me->GetCharmInfo()->IsCommandAttack()) me->SendPetAIReaction(me->GetGUID()); if (chase) { bool oldCmdAttack = me->GetCharmInfo()->IsCommandAttack(); // This needs to be reset after other flags are cleared ClearCharmInfoFlags(); me->GetCharmInfo()->SetIsCommandAttack(oldCmdAttack); // For passive pets commanded to attack so they will use spells if (me->HasUnitState(UNIT_STATE_FOLLOW)) me->GetMotionMaster()->Remove(FOLLOW_MOTION_TYPE); // Pets with ranged attacks should not care about the chase angle at all. float chaseDistance = me->GetPetChaseDistance(); float angle = chaseDistance == 0.f ? float(M_PI) : 0.f; float tolerance = chaseDistance == 0.f ? float(M_PI_4) : float(M_PI * 2); me->GetMotionMaster()->MoveChase(target, ChaseRange(0.f, chaseDistance), ChaseAngle(angle, tolerance)); } else // (Stay && ((Aggressive || Defensive) && In Melee Range))) { ClearCharmInfoFlags(); me->GetCharmInfo()->SetIsAtStay(true); if (me->HasUnitState(UNIT_STATE_FOLLOW)) me->GetMotionMaster()->Remove(FOLLOW_MOTION_TYPE); me->GetMotionMaster()->MoveIdle(); } } } void PetAI::MovementInform(uint32 type, uint32 id) { // Receives notification when pet reaches stay or follow owner switch (type) { case POINT_MOTION_TYPE: { // Pet is returning to where stay was clicked. data should be // pet's GUIDLow since we set that as the waypoint ID if (id == me->GetGUID().GetCounter() && me->GetCharmInfo()->IsReturning()) { ClearCharmInfoFlags(); me->GetCharmInfo()->SetIsAtStay(true); me->GetMotionMaster()->MoveIdle(); } break; } case FOLLOW_MOTION_TYPE: { // If data is owner's GUIDLow then we've reached follow point, // otherwise we're probably chasing a creature if (me->GetCharmerOrOwner() && me->GetCharmInfo() && id == me->GetCharmerOrOwner()->GetGUID().GetCounter() && me->GetCharmInfo()->IsReturning()) { ClearCharmInfoFlags(); me->GetCharmInfo()->SetIsFollowing(true); } break; } default: break; } } bool PetAI::CanAttack(Unit* target) { // Evaluates wether a pet can attack a specific target based on CommandState, ReactState and other flags // IMPORTANT: The order in which things are checked is important, be careful if you add or remove checks // Hmmm... if (!target) return false; if (!target->IsAlive()) { // if target is invalid, pet should evade automaticly // Clear target to prevent getting stuck on dead targets //me->AttackStop(); //me->InterruptNonMeleeSpells(false); return false; } if (!me->GetCharmInfo()) { TC_LOG_WARN("scripts.ai.petai", "me->GetCharmInfo() is NULL in PetAI::CanAttack(). Debug info: {}", GetDebugInfo()); return false; } // Passive - passive pets can attack if told to if (me->HasReactState(REACT_PASSIVE)) return me->GetCharmInfo()->IsCommandAttack(); // CC - mobs under crowd control can be attacked if owner commanded if (target->HasBreakableByDamageCrowdControlAura()) return me->GetCharmInfo()->IsCommandAttack(); // Returning - pets ignore attacks only if owner clicked follow if (me->GetCharmInfo()->IsReturning()) return !me->GetCharmInfo()->IsCommandFollow(); // Stay - can attack if target is within range or commanded to if (me->GetCharmInfo()->HasCommandState(COMMAND_STAY)) return (me->IsWithinMeleeRange(target) || me->GetCharmInfo()->IsCommandAttack()); // Pets attacking something (or chasing) should only switch targets if owner tells them to if (me->GetVictim() && me->GetVictim() != target) { // Check if our owner selected this target and clicked "attack" Unit* ownerTarget = nullptr; if (Player* owner = me->GetCharmerOrOwner()->ToPlayer()) ownerTarget = owner->GetSelectedUnit(); else ownerTarget = me->GetCharmerOrOwner()->GetVictim(); if (ownerTarget && me->GetCharmInfo()->IsCommandAttack()) return (target->GetGUID() == ownerTarget->GetGUID()); } // Follow if (me->GetCharmInfo()->HasCommandState(COMMAND_FOLLOW)) return !me->GetCharmInfo()->IsReturning(); // default, though we shouldn't ever get here return false; } void PetAI::ReceiveEmote(Player* player, uint32 emote) { if (me->GetOwnerGUID() != player->GetGUID()) return; switch (emote) { case TEXT_EMOTE_COWER: if (me->IsPet() && me->ToPet()->IsPetGhoul()) me->HandleEmoteCommand(/*EMOTE_ONESHOT_ROAR*/EMOTE_ONESHOT_OMNICAST_GHOUL); break; case TEXT_EMOTE_ANGRY: if (me->IsPet() && me->ToPet()->IsPetGhoul()) me->HandleEmoteCommand(/*EMOTE_ONESHOT_COWER*/EMOTE_STATE_STUN); break; case TEXT_EMOTE_GLARE: if (me->IsPet() && me->ToPet()->IsPetGhoul()) me->HandleEmoteCommand(EMOTE_STATE_STUN); break; case TEXT_EMOTE_SOOTHE: if (me->IsPet() && me->ToPet()->IsPetGhoul()) me->HandleEmoteCommand(EMOTE_ONESHOT_OMNICAST_GHOUL); break; } } bool PetAI::NeedToStop() { // This is needed for charmed creatures, as once their target was reset other effects can trigger threat if (me->IsCharmed() && me->GetVictim() == me->GetCharmer()) return true; // dont allow pets to follow targets far away from owner if (Unit* owner = me->GetCharmerOrOwner()) if (owner->GetExactDist(me) >= (owner->GetVisibilityRange() - 10.0f)) return true; return !me->IsValidAttackTarget(me->GetVictim()); } void PetAI::StopAttack() { if (!me->IsAlive()) { me->GetMotionMaster()->Clear(); me->GetMotionMaster()->MoveIdle(); me->CombatStop(); return; } me->AttackStop(); me->InterruptNonMeleeSpells(false); me->GetCharmInfo()->SetIsCommandAttack(false); ClearCharmInfoFlags(); HandleReturnMovement(); } void PetAI::UpdateAllies() { _updateAlliesTimer = 10 * IN_MILLISECONDS; // update friendly targets every 10 seconds, lesser checks increase performance Unit* owner = me->GetCharmerOrOwner(); if (!owner) return; Group* group = nullptr; if (Player* player = owner->ToPlayer()) group = player->GetGroup(); // only pet and owner/not in group->ok if (_allySet.size() == 2 && !group) return; // owner is in group; group members filled in already (no raid -> subgroupcount = whole count) if (group && !group->isRaidGroup() && _allySet.size() == (group->GetMembersCount() + 2)) return; _allySet.clear(); _allySet.insert(me->GetGUID()); if (group) // add group { for (GroupReference const& itr : group->GetMembers()) { Player* Target = itr.GetSource(); if (!Target->IsInMap(owner) || !group->SameSubGroup(owner->ToPlayer(), Target)) continue; if (Target->GetGUID() == owner->GetGUID()) continue; _allySet.insert(Target->GetGUID()); } } else // remove group _allySet.insert(owner->GetGUID()); } void PetAI::OnCharmed(bool isNew) { if (!me->isPossessedByPlayer() && me->IsCharmed()) me->GetMotionMaster()->MoveFollow(me->GetCharmer(), PET_FOLLOW_DIST, me->GetFollowAngle()); CreatureAI::OnCharmed(isNew); } void PetAI::ClearCharmInfoFlags() { CharmInfo* ci = me->GetCharmInfo(); if (ci) { ci->SetIsAtStay(false); ci->SetIsCommandAttack(false); ci->SetIsCommandFollow(false); ci->SetIsFollowing(false); ci->SetIsReturning(false); } }