Merge branch 'fix/creature-aggro-range'

This commit is contained in:
DigiD702 2026-08-16 19:22:17 -07:00
commit 9d040aba69
4 changed files with 59 additions and 23 deletions

View file

@ -0,0 +1,26 @@
-- Per-creature aggro detection range.
--
-- Sections:
-- 1. New `creature_template`.`detection_range` column
-- 2. Starting-zone detection ranges
-- ---------------------------------------------------------------------------
-- 1. New `creature_template`.`detection_range` column
-- ---------------------------------------------------------------------------
-- Radius, in yards, at which a creature notices a target of its own level.
-- The core adjusts it by one yard per level of difference and keeps the result
-- inside the 5 to 45 yard range, so 20 reproduces the previous behavior.
ALTER TABLE `creature_template`
ADD COLUMN `detection_range` FLOAT NOT NULL DEFAULT 20 AFTER `ModLevel`;
-- ---------------------------------------------------------------------------
-- 2. Starting-zone detection ranges
-- ---------------------------------------------------------------------------
-- Creatures in the starting-zone level bracket let players come far closer
-- before engaging than the rest of the world does, which keeps a single pull
-- from dragging in the surrounding camp.
UPDATE `creature_template` SET `detection_range`=10 WHERE `maxlevel` <= 6;

View file

@ -13,6 +13,7 @@
#include "CreatureAISelector.h"
#include "CreatureGroups.h"
#include "DatabaseEnv.h"
#include "DBCStores.h"
#include "Formulas.h"
#include "GameEventMgr.h"
#include "GossipDef.h"
@ -1397,12 +1398,11 @@ bool Creature::CanStartAttack(Unit const* who, bool force) const
if (!_IsTargetAcceptable(who))
return false;
if (who->IsInCombat() && IsWithinDist(who, ATTACK_DISTANCE))
if (Unit* victim = who->getAttackerForHelper())
if (IsWithinDistInMap(victim, sWorld->GetFloatConfig(WorldFloatConfigs::CONFIG_CREATURE_FAMILY_ASSISTANCE_RADIUS)))
force = true;
if (IsNeutralToAll())
return false;
if (!force && (IsNeutralToAll() || !IsWithinDistInMap(who, GetAttackDistance(who) + m_CombatDistance)))
// measured between both centers, so the detection range is not silently widened by either model's size
if (!IsInMap(who) || !InSamePhase(who) || GetExactDist(who) > GetAttackDistance(who) + m_CombatDistance)
return false;
}
@ -1418,23 +1418,17 @@ float Creature::GetAttackDistance(Unit const* player) const
if (aggroRate == 0)
return 0.0f;
uint32 playerlevel = player->getLevelForTarget(this);
uint32 creaturelevel = getLevelForTarget(player);
int32 playerlevel = int32(player->getLevelForTarget(this));
int32 creaturelevel = int32(getLevelForTarget(player));
int32 leveldif = creaturelevel - playerlevel;
int32 leveldif = int32(playerlevel) - int32(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 (leveldif < -25)
leveldif = -25;
// "The aggro radius of a mob having the same level as the player is roughly 20 yards"
float RetDistance = 20;
// Blizzard sets the radius per creature; starting zones use much shorter ranges than the rest of the world
float baseAggroDistance = GetCreatureTemplate()->DetectionRange;
// "Aggro Radius varies with level difference at a rate of roughly 1 yard/level"
// radius grow if playlevel < creaturelevel
RetDistance -= (float)leveldif;
float RetDistance = baseAggroDistance + float(leveldif);
if (creaturelevel + 5 <= sWorld->getIntConfig(WorldIntConfigs::CONFIG_MAX_PLAYER_LEVEL))
if (creaturelevel + 5 <= int32(sWorld->getIntConfig(WorldIntConfigs::CONFIG_MAX_PLAYER_LEVEL)))
{
// detect range auras
RetDistance += GetTotalAuraModifier(SPELL_AURA_MOD_DETECT_RANGE);
@ -1443,9 +1437,14 @@ float Creature::GetAttackDistance(Unit const* player) const
RetDistance += player->GetTotalAuraModifier(SPELL_AURA_MOD_DETECTED_RANGE);
}
// "Minimum Aggro Radius for a mob seems to be combat range (5 yards)"
if (RetDistance < 5)
RetDistance = 5;
// Creatures above the level cap of their own expansion (bosses) must not out-range regular creatures.
// Templates whose cap sits below the target's level carry an unusable `exp`, so they are left alone.
int32 expansionMaxLevel = int32(GetMaxLevelForExpansion(GetCreatureTemplate()->expansion));
if (creaturelevel > expansionMaxLevel && expansionMaxLevel >= playerlevel)
RetDistance = baseAggroDistance + float(expansionMaxLevel - playerlevel);
// "Minimum Aggro Radius for a mob seems to be combat range (5 yards)", the maximum is 45 yards
RetDistance = std::max(ATTACK_DISTANCE, std::min(RetDistance, MAX_AGGRO_RADIUS));
return (RetDistance * aggroRate);
}

View file

@ -129,6 +129,7 @@ struct CreatureTemplate
uint32 flags_extra;
uint32 ScriptID;
bool ModLevel;
float DetectionRange; // aggro radius against a target of the same level
uint32 GetRandomValidModelId() const;
uint32 GetFirstValidModelId() const;
@ -429,6 +430,9 @@ typedef std::map<uint32, time_t> CreatureSpellCooldowns;
// max different by z coordinate for creature aggro reaction
#define CREATURE_Z_ATTACK_RANGE 3
// aggro radius against a target of the same level, used when `detection_range` holds no usable value
#define DEFAULT_DETECTION_RANGE 20.0f
#define MAX_VENDOR_ITEMS 150 // Limitation in 4.x.x item count in SMSG_LIST_INVENTORY
class Creature : public Unit, public GridObject<Creature>, public MapObject

View file

@ -452,8 +452,8 @@ void ObjectMgr::LoadCreatureTemplates()
"spell1, spell2, spell3, spell4, spell5, spell6, spell7, spell8, PetSpellDataId, VehicleId, mingold, maxgold, AIName, MovementType, "
// 69 70 71 72 73 74 75 76 77 78 79 80
"InhabitType, HoverHeight, Health_mod, Mana_mod, Mana_mod_extra, Armor_mod, RacialLeader, questItem1, questItem2, questItem3, questItem4, questItem5, "
// 81 82 83 84 85 86
" questItem6, movementId, RegenHealth, mechanic_immune_mask, flags_extra, ScriptName, ModLevel "
// 81 82 83 84 85 86 87 88
" questItem6, movementId, RegenHealth, mechanic_immune_mask, flags_extra, ScriptName, ModLevel, detection_range "
"FROM creature_template;");
if (!result)
@ -557,6 +557,7 @@ void ObjectMgr::LoadCreatureTemplates()
creatureTemplate.flags_extra = fields[85].GetUInt32();
creatureTemplate.ScriptID = GetScriptId(fields[86].GetCString());
creatureTemplate.ModLevel = fields[87].GetBool();
creatureTemplate.DetectionRange = fields[88].GetFloat();
++count;
} while (result->NextRow());
@ -893,6 +894,12 @@ void ObjectMgr::CheckCreatureTemplate(CreatureTemplate const* cInfo)
const_cast<CreatureTemplate*>(cInfo)->HoverHeight = 1.0f;
}
if (cInfo->DetectionRange <= 0.0f || cInfo->DetectionRange > MAX_AGGRO_RADIUS)
{
SF_LOG_ERROR("sql.sql", "Creature (Entry: %u) has wrong value (%f) in `detection_range`, set to %f.", cInfo->Entry, cInfo->DetectionRange, DEFAULT_DETECTION_RANGE);
const_cast<CreatureTemplate*>(cInfo)->DetectionRange = DEFAULT_DETECTION_RANGE;
}
if (cInfo->VehicleId)
{
VehicleEntry const* vehId = sVehicleStore.LookupEntry(cInfo->VehicleId);