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 ec178aa511..ecaaf4b928 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" @@ -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); } diff --git a/src/server/game/Entities/Creature/Creature.h b/src/server/game/Entities/Creature/Creature.h index 89ca0f802e..5480bf68fa 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; @@ -429,6 +430,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 0e05b6d829..c0fb0ac715 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);