From 8f2a0f6b805cf7979b3cfce89f458d270fd91341 Mon Sep 17 00:00:00 2001 From: DigiD702 Date: Sun, 16 Aug 2026 14:50:32 -0700 Subject: [PATCH 1/3] [Core/Creature] Fix aggro radius counting creature size twice. Aggro checks compare the attack distance with IsWithinDistInMap, which adds both units' combat reaches on top of it, so large creatures pulled from much further away than small ones of the same level. Subtract the creature's own combat reach from the 20 yard base radius, clamp the result to 5-45 yards, and cap creatures above their expansion's max level so bosses no longer out-range regular mobs. --- .../game/Entities/Creature/Creature.cpp | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 2715255525..7f9b3e00db 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -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" @@ -1418,23 +1419,19 @@ 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; + // "The aggro radius of a mob having the same level as the player is roughly 20 yards". + // The callers add both combat reaches on top of this distance, so drop our own reach here + // to keep large models from aggroing further than small ones. + float baseAggroDistance = 20.0f - GetCombatReach(); // "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 +1440,13 @@ 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 + int32 expansionMaxLevel = int32(GetMaxLevelForExpansion(GetCreatureTemplate()->expansion)); + if (expansionMaxLevel > 0 && creaturelevel > expansionMaxLevel) + 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); } From 3eb2b53523fe2b617b0ff92f918644a175a9b084 Mon Sep 17 00:00:00 2001 From: DigiD702 Date: Sun, 16 Aug 2026 14:52:30 -0700 Subject: [PATCH 2/3] [Core/Creature] Stop creatures force-attacking players fighting nearby. Any creature standing within 5 yards of a player already in combat was pulled into that fight regardless of its faction or its own aggro radius, so a single pull cascaded through a camp. Delayed assistance from CallAssistance still brings in valid nearby allies, which keeps the intended pack behavior. --- src/server/game/Entities/Creature/Creature.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 7f9b3e00db..97b83e5601 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -1398,12 +1398,7 @@ 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 (!force && (IsNeutralToAll() || !IsWithinDistInMap(who, GetAttackDistance(who) + m_CombatDistance))) + if (IsNeutralToAll() || !IsWithinDistInMap(who, GetAttackDistance(who) + m_CombatDistance)) return false; } From 839c5275fb785ad3377070bd378a8d03f64d0e0d Mon Sep 17 00:00:00 2001 From: DigiD702 Date: Sun, 16 Aug 2026 15:29:30 -0700 Subject: [PATCH 3/3] [Core/Creature] Read aggro radius from creature_template.detection_range. The same-level aggro radius was a single hardcoded 20 yards for every creature, so starting-zone mobs noticed players from across the camp and one pull dragged in its neighbors. The radius is now a per-creature value defaulting to 20, with the starting-zone level bracket set to 10, and the range check measures center to center instead of adding both models' combat reaches on top of it. --- ...6_08_16_world_creature_detection_range.sql | 26 +++++++++++++++++++ .../game/Entities/Creature/Creature.cpp | 17 +++++++----- src/server/game/Entities/Creature/Creature.h | 4 +++ src/server/game/Globals/ObjectMgr.cpp | 11 ++++++-- 4 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 sql/pending_updates/world/2026_08_16_world_creature_detection_range.sql diff --git a/sql/pending_updates/world/2026_08_16_world_creature_detection_range.sql b/sql/pending_updates/world/2026_08_16_world_creature_detection_range.sql new file mode 100644 index 0000000000..c3dddd07c1 --- /dev/null +++ b/sql/pending_updates/world/2026_08_16_world_creature_detection_range.sql @@ -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; diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index 97b83e5601..3e40e230e0 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -1398,7 +1398,11 @@ bool Creature::CanStartAttack(Unit const* who, bool force) const if (!_IsTargetAcceptable(who)) return false; - if (IsNeutralToAll() || !IsWithinDistInMap(who, GetAttackDistance(who) + m_CombatDistance)) + if (IsNeutralToAll()) + return false; + + // 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,10 +1422,8 @@ float Creature::GetAttackDistance(Unit const* player) const int32 creaturelevel = int32(getLevelForTarget(player)); int32 leveldif = creaturelevel - playerlevel; - // "The aggro radius of a mob having the same level as the player is roughly 20 yards". - // The callers add both combat reaches on top of this distance, so drop our own reach here - // to keep large models from aggroing further than small ones. - float baseAggroDistance = 20.0f - GetCombatReach(); + // 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" float RetDistance = baseAggroDistance + float(leveldif); @@ -1435,9 +1437,10 @@ float Creature::GetAttackDistance(Unit const* player) const RetDistance += player->GetTotalAuraModifier(SPELL_AURA_MOD_DETECTED_RANGE); } - // Creatures above the level cap of their own expansion (bosses) must not out-range regular creatures + // 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 (expansionMaxLevel > 0 && creaturelevel > expansionMaxLevel) + 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 diff --git a/src/server/game/Entities/Creature/Creature.h b/src/server/game/Entities/Creature/Creature.h index 490928ac6a..1d72e8ab15 100644 --- a/src/server/game/Entities/Creature/Creature.h +++ b/src/server/game/Entities/Creature/Creature.h @@ -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; @@ -421,6 +422,9 @@ typedef std::map 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, public MapObject diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index 5728a03de3..efd00d869a 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -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(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(cInfo)->DetectionRange = DEFAULT_DETECTION_RANGE; + } + if (cInfo->VehicleId) { VehicleEntry const* vehId = sVehicleStore.LookupEntry(cInfo->VehicleId);