diff --git a/src/server/game/AI/CreatureAI.cpp b/src/server/game/AI/CreatureAI.cpp index a178d00da..7dd7a5f19 100644 --- a/src/server/game/AI/CreatureAI.cpp +++ b/src/server/game/AI/CreatureAI.cpp @@ -1,8 +1,8 @@ /* - * Copyright (C) 2016+ AzerothCore , released under GNU GPL v2 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-GPL2 - * Copyright (C) 2008-2016 TrinityCore - * Copyright (C) 2005-2009 MaNGOS - */ +* Copyright (C) 2016+ AzerothCore , released under GNU GPL v2 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-GPL2 +* Copyright (C) 2008-2016 TrinityCore +* Copyright (C) 2005-2009 MaNGOS +*/ #include "CreatureAI.h" #include "CreatureAIImpl.h" @@ -127,6 +127,26 @@ void CreatureAI::MoveInLineOfSight(Unit* who) AttackStart(who); } +// 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())); + me->StopMoving(); + me->GetMotionMaster()->MoveDistract(5 * IN_MILLISECONDS); +} + void CreatureAI::EnterEvadeMode() { if (!_EnterEvadeMode()) diff --git a/src/server/game/AI/CreatureAI.h b/src/server/game/AI/CreatureAI.h index 38e4847fd..28303b573 100644 --- a/src/server/game/AI/CreatureAI.h +++ b/src/server/game/AI/CreatureAI.h @@ -1,8 +1,8 @@ /* - * Copyright (C) 2016+ AzerothCore , released under GNU GPL v2 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-GPL2 - * Copyright (C) 2008-2016 TrinityCore - * Copyright (C) 2005-2009 MaNGOS - */ +* Copyright (C) 2016+ AzerothCore , released under GNU GPL v2 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-GPL2 +* Copyright (C) 2008-2016 TrinityCore +* Copyright (C) 2005-2009 MaNGOS +*/ #ifndef TRINITY_CREATUREAI_H #define TRINITY_CREATUREAI_H @@ -76,6 +76,9 @@ class CreatureAI : public UnitAI // Called if IsVisible(Unit* who) is true at each who move, reaction at visibility zone enter void MoveInLineOfSight_Safe(Unit* who); + // Trigger Creature "Alert" state (creature can see stealthed unit) + void TriggerAlert(Unit const* who) const; + // Called in Creature::Update when deathstate = DEAD. Inherited classes may maniuplate the ability to respawn based on scripted events. virtual bool CanRespawn() { return true; } diff --git a/src/server/game/AI/ScriptedAI/ScriptedEscortAI.cpp b/src/server/game/AI/ScriptedAI/ScriptedEscortAI.cpp index c0184de03..ff482462c 100644 --- a/src/server/game/AI/ScriptedAI/ScriptedEscortAI.cpp +++ b/src/server/game/AI/ScriptedAI/ScriptedEscortAI.cpp @@ -96,7 +96,15 @@ void npc_escortAI::MoveInLineOfSight(Unit* who) return; if (me->CanStartAttack(who)) + { + if (me->HasUnitState(UNIT_STATE_DISTRACTED)) + { + me->ClearUnitState(UNIT_STATE_DISTRACTED); + me->GetMotionMaster()->Clear(); + } AttackStart(who); + } + } void npc_escortAI::JustDied(Unit* /*killer*/) diff --git a/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.cpp b/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.cpp index 2d3163fad..f1cf37b41 100644 --- a/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.cpp +++ b/src/server/game/AI/ScriptedAI/ScriptedFollowerAI.cpp @@ -89,7 +89,14 @@ void FollowerAI::MoveInLineOfSight(Unit* who) return; if (me->CanStartAttack(who)) + { + if (me->HasUnitState(UNIT_STATE_DISTRACTED)) + { + me->ClearUnitState(UNIT_STATE_DISTRACTED); + me->GetMotionMaster()->Clear(); + } AttackStart(who); + } } void FollowerAI::JustDied(Unit* /*pKiller*/) diff --git a/src/server/game/AI/SmartScripts/SmartAI.cpp b/src/server/game/AI/SmartScripts/SmartAI.cpp index d3838453c..a6849294f 100644 --- a/src/server/game/AI/SmartScripts/SmartAI.cpp +++ b/src/server/game/AI/SmartScripts/SmartAI.cpp @@ -674,7 +674,14 @@ void SmartAI::MoveInLineOfSight(Unit* who) return; if (me->CanStartAttack(who)) + { + if (me->HasUnitState(UNIT_STATE_DISTRACTED)) + { + me->ClearUnitState(UNIT_STATE_DISTRACTED); + me->GetMotionMaster()->Clear(); + } AttackStart(who); + } } bool SmartAI::CanAIAttack(const Unit* /*who*/) const diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 54d83e9b7..9e1b3ab12 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -2809,3 +2809,45 @@ void Creature::ReleaseFocus(Spell const* focusSpell) if (focusSpell->GetSpellInfo()->HasAttribute(SPELL_ATTR5_DONT_TURN_DURING_CAST)) ClearUnitState(UNIT_STATE_ROTATING); } + +float Creature::GetAttackDistance(Unit const* player) const +{ + float aggroRate = sWorld->getRate(RATE_CREATURE_AGGRO); + + if (aggroRate == 0) + return 0.0f; + + if (!player) + return 0.0f; + + uint32 playerLevel = player->getLevelForTarget(this); + uint32 creatureLevel = getLevelForTarget(player); + + int32 levelDiff = static_cast(playerLevel) - static_cast(creatureLevel); + + // "The maximum Aggro Radius has a cap of 25 levels under. Example: A level 30 char has the same Aggro Radius of a level 5 char on a level 60 mob." + if (levelDiff < -25) + levelDiff = -25; + + // "The aggro radius of a mob having the same level as the player is roughly 20 yards" + float retDistance = 20.0f; + + // "Aggro Radius varies with level difference at a rate of roughly 1 yard/level" + // radius grow if playlevel < creaturelevel + retDistance -= static_cast(levelDiff); + + if (creatureLevel + 5 <= sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) + { + // detect range auras + retDistance += static_cast( GetTotalAuraModifier(SPELL_AURA_MOD_DETECT_RANGE) ); + + // detected range auras + retDistance += static_cast( player->GetTotalAuraModifier(SPELL_AURA_MOD_DETECTED_RANGE) ); + } + + // "Minimum Aggro Radius for a mob seems to be combat range (5 yards)" + if (retDistance < 5.0f) + retDistance = 5.0f; + + return (retDistance*aggroRate); +} diff --git a/src/server/game/Entities/Creature/Creature.h b/src/server/game/Entities/Creature/Creature.h index 50ed18864..b99e4e391 100644 --- a/src/server/game/Entities/Creature/Creature.h +++ b/src/server/game/Entities/Creature/Creature.h @@ -596,6 +596,7 @@ class Creature : public Unit, public GridObject, public MovableMapObje bool CanStartAttack(Unit const* u) const; float GetAggroRange(Unit const* target) const; + float GetAttackDistance(Unit const* player) const; void SendAIReaction(AiReaction reactionType); diff --git a/src/server/game/Entities/Object/Object.cpp b/src/server/game/Entities/Object/Object.cpp index 15be17a5a..e113c350e 100644 --- a/src/server/game/Entities/Object/Object.cpp +++ b/src/server/game/Entities/Object/Object.cpp @@ -1551,8 +1551,8 @@ float WorldObject::GetSightRange(const WorldObject* target) const return 0.0f; } -bool WorldObject::CanSeeOrDetect(WorldObject const* obj, bool ignoreStealth, bool distanceCheck) const -{ +bool WorldObject::CanSeeOrDetect(WorldObject const* obj, bool ignoreStealth, bool distanceCheck, bool checkAlert) const +{ if (this == obj) return true; @@ -1647,12 +1647,12 @@ bool WorldObject::CanSeeOrDetect(WorldObject const* obj, bool ignoreStealth, boo if (obj->IsInvisibleDueToDespawn()) return false; - // pussywizard: arena spectator + // pussywizard: arena spectator if (this->GetTypeId() == TYPEID_PLAYER) if (((const Player*)this)->IsSpectator() && ((const Player*)this)->FindMap()->IsBattleArena() && (obj->m_invisibility.GetFlags() || obj->m_stealth.GetFlags())) return false; - if (!CanDetect(obj, ignoreStealth, !distanceCheck)) + if (!CanDetect(obj, ignoreStealth, !distanceCheck, checkAlert)) return false; return true; @@ -1665,7 +1665,7 @@ bool WorldObject::CanNeverSee(WorldObject const* obj) const return GetMap() != obj->GetMap() || !InSamePhase(obj); } -bool WorldObject::CanDetect(WorldObject const* obj, bool ignoreStealth, bool checkClient) const +bool WorldObject::CanDetect(WorldObject const* obj, bool ignoreStealth, bool checkClient, bool checkAlert) const { const WorldObject* seer = this; @@ -1682,7 +1682,7 @@ bool WorldObject::CanDetect(WorldObject const* obj, bool ignoreStealth, bool che if (!seer->CanDetectInvisibilityOf(obj)) // xinef: added ignoreStealth, allow AoE spells to hit invisible targets! return false; - if (!seer->CanDetectStealthOf(obj)) + if (!seer->CanDetectStealthOf(obj, checkAlert)) { // xinef: ignore units players have at client, this cant be cheated! if (checkClient) @@ -1740,7 +1740,7 @@ bool WorldObject::CanDetectInvisibilityOf(WorldObject const* obj) const return true; } -bool WorldObject::CanDetectStealthOf(WorldObject const* obj) const +bool WorldObject::CanDetectStealthOf(WorldObject const* obj, bool checkAlert) const { // Combat reach is the minimal distance (both in front and behind), // and it is also used in the range calculation. @@ -1797,9 +1797,22 @@ bool WorldObject::CanDetectStealthOf(WorldObject const* obj) const // Calculate max distance float visibilityRange = float(detectionValue) * 0.3f + combatReach; - if (visibilityRange > MAX_PLAYER_STEALTH_DETECT_RANGE) + Unit const* unit = ToUnit(); + + // If this unit is an NPC then player detect range doesn't apply + if (unit && unit->GetTypeId() == TYPEID_PLAYER && visibilityRange > MAX_PLAYER_STEALTH_DETECT_RANGE) visibilityRange = MAX_PLAYER_STEALTH_DETECT_RANGE; + if (checkAlert) + visibilityRange += (visibilityRange * 0.08f) + 1.5f; + + + Unit const* targetUnit = obj->ToUnit(); + + // If checking for alert, and creature's visibility range is greater than aggro distance, No alert + if (checkAlert && unit && unit->ToCreature() && visibilityRange >= unit->ToCreature()->GetAttackDistance(targetUnit) + unit->ToCreature()->m_CombatDistance) + return false; + if (distance > visibilityRange) return false; } diff --git a/src/server/game/Entities/Object/Object.h b/src/server/game/Entities/Object/Object.h index 4273d173f..0d779cd75 100644 --- a/src/server/game/Entities/Object/Object.h +++ b/src/server/game/Entities/Object/Object.h @@ -906,7 +906,8 @@ class WorldObject : public Object, public WorldLocation float GetGridActivationRange() const; float GetVisibilityRange() const; float GetSightRange(const WorldObject* target = NULL) const; - bool CanSeeOrDetect(WorldObject const* obj, bool ignoreStealth = false, bool distanceCheck = false) const; + //bool CanSeeOrDetect(WorldObject const* obj, bool ignoreStealth = false, bool distanceCheck = false) const; + bool CanSeeOrDetect(WorldObject const* obj, bool ignoreStealth = false, bool distanceCheck = false, bool checkAlert = false) const; FlaggedValuesArray32 m_stealth; FlaggedValuesArray32 m_stealthDetect; @@ -1047,9 +1048,11 @@ class WorldObject : public Object, public WorldLocation bool CanNeverSee(WorldObject const* obj) const; virtual bool CanAlwaysSee(WorldObject const* /*obj*/) const { return false; } - bool CanDetect(WorldObject const* obj, bool ignoreStealth, bool checkClient) const; + //bool CanDetect(WorldObject const* obj, bool ignoreStealth, bool checkClient) const; + bool CanDetect(WorldObject const* obj, bool ignoreStealth, bool checkClient, bool checkAlert = false) const; bool CanDetectInvisibilityOf(WorldObject const* obj) const; - bool CanDetectStealthOf(WorldObject const* obj) const; + //bool CanDetectStealthOf(WorldObject const* obj) const; + bool CanDetectStealthOf(WorldObject const* obj, bool checkAlert = false) const; }; namespace Trinity diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index bb7a89f13..553299330 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -12648,7 +12648,8 @@ bool Unit::_IsValidAttackTarget(Unit const* target, SpellInfo const* bySpell, Wo return false; // can't attack invisible (ignore stealth for aoe spells) also if the area being looked at is from a spell use the dynamic object created instead of the casting unit. - if ((!bySpell || !bySpell->HasAttribute(SPELL_ATTR6_CAN_TARGET_INVISIBLE)) && (obj ? !obj->CanSeeOrDetect(target, bySpell && bySpell->IsAffectingArea() && !bySpell->HasAttribute(SPELL_ATTR3_NO_INITIAL_AGGRO)) : !CanSeeOrDetect(target, bySpell && bySpell->IsAffectingArea() && !bySpell->HasAttribute(SPELL_ATTR3_NO_INITIAL_AGGRO)))) + //Ignore stealth if target is player and unit in combat with same player + if ((!bySpell || !bySpell->HasAttribute(SPELL_ATTR6_CAN_TARGET_INVISIBLE)) && (obj ? !obj->CanSeeOrDetect(target, bySpell && bySpell->IsAffectingArea()) : !CanSeeOrDetect(target, (bySpell && bySpell->IsAffectingArea()) || (target->GetTypeId() == TYPEID_PLAYER && target->HasStealthAura() && target->IsInCombat() && IsInCombatWith(target))))) return false; // can't attack dead @@ -19449,3 +19450,22 @@ void Unit::setRace(uint8 race) if (GetTypeId() == TYPEID_PLAYER) m_race = race; } + +// Check if unit in combat with specific unit +bool Unit::IsInCombatWith(Unit const* who) const +{ + // Check target exists + if (!who) + return false; + // Search in threat list + uint64 guid = who->GetGUID(); + for (ThreatContainer::StorageType::const_iterator i = m_ThreatManager.getThreatList().begin(); i != m_ThreatManager.getThreatList().end(); ++i) + { + HostileReference* ref = (*i); + // Return true if the unit matches + if (ref && ref->getUnitGuid() == guid) + return true; + } + // Nothing found, false. + return false; +} diff --git a/src/server/game/Entities/Unit/Unit.h b/src/server/game/Entities/Unit/Unit.h index 4db5ae4af..b9e7d6f47 100644 --- a/src/server/game/Entities/Unit/Unit.h +++ b/src/server/game/Entities/Unit/Unit.h @@ -1687,6 +1687,8 @@ class Unit : public WorldObject bool IsInFlight() const { return HasUnitState(UNIT_STATE_IN_FLIGHT); } bool IsInCombat() const { return HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IN_COMBAT); } + bool IsInCombatWith(Unit const* who) const; + bool IsPetInCombat() const { return HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PET_IN_COMBAT); } void CombatStart(Unit* target, bool initialAggro = true); void CombatStartOnCast(Unit* target, bool initialAggro = true, uint32 duration = 0); diff --git a/src/server/game/Grids/Notifiers/GridNotifiers.cpp b/src/server/game/Grids/Notifiers/GridNotifiers.cpp index 10732ea98..5ab006483 100644 --- a/src/server/game/Grids/Notifiers/GridNotifiers.cpp +++ b/src/server/game/Grids/Notifiers/GridNotifiers.cpp @@ -128,8 +128,17 @@ inline void CreatureUnitRelocationWorker(Creature* c, Unit* u) return; if (c->HasReactState(REACT_AGGRESSIVE) && !c->HasUnitState(UNIT_STATE_SIGHTLESS)) + { if (c->IsAIEnabled && c->CanSeeOrDetect(u, false, true)) + { c->AI()->MoveInLineOfSight_Safe(u); + } + else + { + if (u->GetTypeId() == TYPEID_PLAYER && u->HasStealthAura() && c->IsAIEnabled && c->CanSeeOrDetect(u, false, true, true)) + c->AI()->TriggerAlert(u); + } + } } void PlayerRelocationNotifier::Visit(PlayerMapType &m) diff --git a/src/server/game/Movement/MovementGenerators/IdleMovementGenerator.cpp b/src/server/game/Movement/MovementGenerators/IdleMovementGenerator.cpp index 35ff60c1c..164749289 100644 --- a/src/server/game/Movement/MovementGenerators/IdleMovementGenerator.cpp +++ b/src/server/game/Movement/MovementGenerators/IdleMovementGenerator.cpp @@ -69,12 +69,23 @@ void RotateMovementGenerator::Finalize(Unit* unit) void DistractMovementGenerator::Initialize(Unit* owner) { + // Distracted creatures stand up if not standing + if (!owner->IsStandState()) + owner->SetStandState(UNIT_STAND_STATE_STAND); + owner->AddUnitState(UNIT_STATE_DISTRACTED); } void DistractMovementGenerator::Finalize(Unit* owner) { owner->ClearUnitState(UNIT_STATE_DISTRACTED); + + // If this is a creature, then return orientation to original position (for idle movement creatures) + if (owner->GetTypeId() == TYPEID_UNIT && owner->ToCreature()) + { + float angle = owner->ToCreature()->GetHomePosition().GetOrientation(); + owner->SetFacingTo(angle); + } } bool DistractMovementGenerator::Update(Unit* owner, uint32 time_diff)