local ____lualib = require("lualib_bundle") local __TS__Class = ____lualib.__TS__Class local Map = ____lualib.Map local __TS__New = ____lualib.__TS__New local __TS__ArraySome = ____lualib.__TS__ArraySome local __TS__ArrayFilter = ____lualib.__TS__ArrayFilter local __TS__StringStartsWith = ____lualib.__TS__StringStartsWith local __TS__ArrayForEach = ____lualib.__TS__ArrayForEach local __TS__StringIncludes = ____lualib.__TS__StringIncludes local __TS__Iterator = ____lualib.__TS__Iterator local __TS__ArrayIndexOf = ____lualib.__TS__ArrayIndexOf local __TS__ArraySplice = ____lualib.__TS__ArraySplice local __TS__StringReplace = ____lualib.__TS__StringReplace local __TS__ParseInt = ____lualib.__TS__ParseInt local __TS__Number = ____lualib.__TS__Number local __TS__NumberIsNaN = ____lualib.__TS__NumberIsNaN local __TS__ArrayReduce = ____lualib.__TS__ArrayReduce local __TS__ArrayFrom = ____lualib.__TS__ArrayFrom local ____exports = {} ____exports.SpawnManager = __TS__Class() local SpawnManager = ____exports.SpawnManager SpawnManager.name = "SpawnManager" SpawnManager.____file_path = "scripts/vscripts/SpawnManager.lua" function SpawnManager.prototype.____constructor(self) self.spawnConfigs = __TS__New(Map) self.spawnTimers = __TS__New(Map) self.spawnedUnits = __TS__New(Map) self.unitGroups = __TS__New(Map) self.zoneRespawnScheduled = __TS__New(Map) self.zoneTimerHandles = __TS__New(Map) self.debugZonesVisible = false self.spawnConfigs = __TS__New(Map) self.spawnedUnits = __TS__New(Map) self.spawnTimers = __TS__New(Map) ListenToGameEvent( "entity_killed", function(event) local killedIndex = event.entindex_killed if killedIndex == nil or killedIndex == nil then return end local killedUnit = EntIndexToHScript(killedIndex) if not killedUnit or not IsValidEntity(killedUnit) then return end self.spawnConfigs:forEach(function(____, config, zoneId) local units = self.spawnedUnits:get(zoneId) or ({}) local wasInZone = __TS__ArraySome( units, function(____, unit) return unit == killedUnit end ) if wasInZone then local aliveUnits = __TS__ArrayFilter( units, function(____, unit) return unit ~= nil and unit ~= killedUnit and unit:IsAlive() end ) self.spawnedUnits:set(zoneId, aliveUnits) self:removeUnitFromGroup(killedUnit) self:QueueRespawn(zoneId) end end) end, nil ) ListenToGameEvent( "entity_hurt", function(event) local hurtIndex = event.entindex_victim local attackerIndex = event.entindex_attacker if hurtIndex == nil or hurtIndex == nil then return end if attackerIndex == nil or attackerIndex == nil then return end local hurtUnit = EntIndexToHScript(hurtIndex) local attacker = EntIndexToHScript(attackerIndex) if not hurtUnit or not attacker or not IsValidEntity(hurtUnit) or not IsValidEntity(attacker) then return end self.spawnConfigs:forEach(function(____, config, zoneId) local units = self.spawnedUnits:get(zoneId) or ({}) local wasInZone = __TS__ArraySome( units, function(____, unit) return unit == hurtUnit end ) if wasInZone and hurtUnit:IsAlive() and attacker:IsAlive() then if hurtUnit:GetTeamNumber() ~= attacker:GetTeamNumber() then hurtUnit:SetContextNum( "last_attacker", attacker:entindex(), 5 ) if config.moveInGroups then local group = self:getGroupForUnit(hurtUnit) if group then for ____, mate in ipairs(group) do if mate ~= hurtUnit and mate:IsAlive() and IsValidEntity(mate) and mate:GetTeamNumber() ~= attacker:GetTeamNumber() then if mate:GetAttackCapability() ~= DOTA_UNIT_CAP_NO_ATTACK then ExecuteOrderFromTable({ UnitIndex = mate:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET, TargetIndex = attacker:entindex(), Queue = false }) end end end end end end end end) end, nil ) self:Think() end function SpawnManager.getInstance(self) if not ____exports.SpawnManager.instance then ____exports.SpawnManager.instance = __TS__New(____exports.SpawnManager) end return ____exports.SpawnManager.instance end function SpawnManager.prototype.Initialize(self) local spawnPoints = {} local currentEntity = Entities:First() while currentEntity ~= nil do local entityName = currentEntity:GetName() if __TS__StringStartsWith(entityName, "spawn_point_") then if not spawnPoints[entityName] then spawnPoints[entityName] = {} end local ____spawnPoints_entityName_0 = spawnPoints[entityName] ____spawnPoints_entityName_0[#____spawnPoints_entityName_0 + 1] = currentEntity:GetAbsOrigin() end currentEntity = Entities:Next(currentEntity) end if spawnPoints.spawn_point_witch then __TS__ArrayForEach( spawnPoints.spawn_point_witch, function(____, position, index) local zoneId = "zone_witch_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_witch", count = 1}}, position = position, radius = 650, interval = 300, team = DOTA_TEAM_BADGUYS, behavior = "aggressive" }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_pigs then __TS__ArrayForEach( spawnPoints.spawn_point_pigs, function(____, position, index) local zoneId = "zone_pigs_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_pig", count = 5}}, position = position, radius = 600, width = 2400, height = 1100, interval = 5, team = DOTA_TEAM_NEUTRALS, behavior = "friendly", shape = "square" }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_sheep then __TS__ArrayForEach( spawnPoints.spawn_point_sheep, function(____, position, index) local zoneId = "zone_sheep_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_sheep", count = 5, canUseAbilities = true}}, position = position, radius = 600, interval = 5, team = DOTA_TEAM_NEUTRALS, behavior = "aggressive", shape = "square" }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_wolf then __TS__ArrayForEach( spawnPoints.spawn_point_wolf, function(____, position, index) local zoneId = "zone_wolf_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_wolf", count = 6, groupKey = "wolf"}, {unitName = "npc_ent", count = 2}}, position = position, radius = 1500, interval = 7, team = DOTA_TEAM_NEUTRALS, behavior = "aggressive", moveInGroups = {groupSize = 3, stayRadius = 280} }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_chicken then __TS__ArrayForEach( spawnPoints.spawn_point_chicken, function(____, position, index) local zoneId = "zone_chicken_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_chicken", count = 8, canUseAbilities = true}}, position = position, radius = 1500, interval = 8, team = DOTA_TEAM_NEUTRALS, behavior = "aggressive", shape = "square", width = 1000, height = 2450 }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_skeletons then __TS__ArrayForEach( spawnPoints.spawn_point_skeletons, function(____, position, index) local zoneId = "zone_skeletons_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_skeleton_zombie_undead", count = 1, canUseAbilities = true}, {unitName = "npc_skeleton_zombie_half_undead", count = 1, canUseAbilities = true}, {unitName = "npc_dead_skeleton_undead", count = 1, canUseAbilities = true}, {unitName = "npc_dead_skeleton_archer_undead", count = 1, canUseAbilities = true}}, position = position, radius = 600, width = 1000, height = 3300, interval = 5, team = DOTA_TEAM_NEUTRALS, behavior = "aggressive", shape = "square" }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_skeletons2 then __TS__ArrayForEach( spawnPoints.spawn_point_skeletons2, function(____, position, index) local zoneId = "zone_skeletons2_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_skeleton_zombie_undead", count = 1, canUseAbilities = true}, {unitName = "npc_skeleton_zombie_half_undead", count = 1, canUseAbilities = true}, {unitName = "npc_dead_skeleton_undead", count = 1, canUseAbilities = true}, {unitName = "npc_dead_skeleton_archer_undead", count = 1, canUseAbilities = true}}, position = position, radius = 600, width = 2000, height = 2000, interval = 5, team = DOTA_TEAM_NEUTRALS, behavior = "aggressive", shape = "square" }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_fish then __TS__ArrayForEach( spawnPoints.spawn_point_fish, function(____, position, index) local zoneId = "zone_fish_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_fish_1", count = 4}, {unitName = "npc_fish_2", count = 4}, {unitName = "npc_bomb", count = 6}}, position = position, radius = 1500, interval = 8, team = DOTA_TEAM_NEUTRALS, behavior = "friendly" }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_wisps then __TS__ArrayForEach( spawnPoints.spawn_point_wisps, function(____, position, index) local zoneId = "zone_wisps_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_wisps", count = 6}}, position = position, radius = 1200, interval = 8, team = DOTA_TEAM_NEUTRALS, behavior = "friendly" }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_thief then __TS__ArrayForEach( spawnPoints.spawn_point_thief, function(____, position, index) local zoneId = "zone_thief_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_thief_leader", count = 1, canUseAbilities = true, groupKey = "thief"}, {unitName = "npc_thief_archer", count = 2, canUseAbilities = true, groupKey = "thief"}, { unitName = "npc_thief_backer", count = 2, canUseAbilities = true, groupKey = "thief", ignoreAIWhileHasModifiers = {"modifier_spirit_breaker_charge_of_darkness"} }}, position = position, radius = 600, width = 1200, height = 15500, interval = 180, team = DOTA_TEAM_NEUTRALS, behavior = "aggressive", shape = "square", moveInGroups = {groupSize = 5, stayRadius = 450} }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_dragons then __TS__ArrayForEach( spawnPoints.spawn_point_dragons, function(____, position, index) local zoneId = "zone_dragons_" .. tostring(index) self:AddSpawnZone(zoneId, { units = { {unitName = "npc_black_dragon", count = 2, canUseAbilities = true, kiteRetreatDistance = 250}, {unitName = "npc_red_dragon", count = 2}, {unitName = "npc_blue_dragon_small", count = 2, canUseAbilities = true}, {unitName = "npc_blue_dragon", count = 2, canUseAbilities = true}, {unitName = "npc_red_dragon_small", count = 2, kiteRetreatDistance = 550} }, position = position, radius = 600, width = 3400, height = 3400, interval = 15, team = DOTA_TEAM_NEUTRALS, behavior = "aggressive", shape = "square" }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_small_frog then __TS__ArrayForEach( spawnPoints.spawn_point_small_frog, function(____, position, index) local zoneId = "zone_small_frog_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_frogman_magi", count = 3, canUseAbilities = true}, {unitName = "npc_frop_tadpole", count = 3, canUseAbilities = true}, {unitName = "npc_small_frog_froglet", count = 2, canUseAbilities = true}, {unitName = "npc_mini_frog", count = 2, canUseAbilities = true}}, position = position, radius = 600, width = 3400, height = 3400, interval = 6, team = DOTA_TEAM_NEUTRALS, behavior = "aggressive", shape = "square" }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end if spawnPoints.spawn_point_snakes_spiders then __TS__ArrayForEach( spawnPoints.spawn_point_snakes_spiders, function(____, position, index) local zoneId = "zone_snakes_spiders_" .. tostring(index) self:AddSpawnZone(zoneId, { units = {{unitName = "npc_venomancer_brute", count = 3, canUseAbilities = true, groupKey = "snakes"}, {unitName = "npc_ravenous_woodfang", count = 3, canUseAbilities = true, groupKey = "spiders"}, {unitName = "npc_lycosidae_stalker", count = 3, canUseAbilities = true, groupKey = "snakes"}}, position = position, radius = 3500, interval = 8, team = DOTA_TEAM_NEUTRALS, behavior = "aggressive", moveInGroups = {groupSize = 5, stayRadius = 250} }) local config = self.spawnConfigs:get(zoneId) self:SpawnInitialUnits(zoneId, config) end ) end end function SpawnManager.prototype.AddSpawnZone(self, id, config) self.spawnConfigs:set(id, config) self.spawnedUnits:set(id, {}) if self.debugZonesVisible then self:DrawSpawnZoneDebug(id, config) end end function SpawnManager.prototype.CreateZoneFromAdmin(self, id, config) self:AddSpawnZone(id, config) self:SpawnInitialUnits(id, config) end function SpawnManager.prototype.ToggleDebugZones(self) self.debugZonesVisible = not self.debugZonesVisible if self.debugZonesVisible then self.spawnConfigs:forEach(function(____, config, id) self:DrawSpawnZoneDebug(id, config) end) else DebugDrawClear() end end function SpawnManager.prototype.Think(self) Timers:CreateTimer( 1, function() self.spawnConfigs:forEach(function(____, _, zoneId) local units = self.spawnedUnits:get(zoneId) or ({}) local validUnits = __TS__ArrayFilter( units, function(____, unit) return unit and IsValidEntity(unit) and unit:IsAlive() end ) self.spawnedUnits:set(zoneId, validUnits) end) self.unitGroups:forEach(function(____, arr, groupId) local valid = __TS__ArrayFilter( arr, function(____, u) return u and IsValidEntity(u) and u:IsAlive() end ) if #valid == 0 then self.unitGroups:delete(groupId) else self.unitGroups:set(groupId, valid) end end) return 1 end ) end function SpawnManager.prototype.startZoneTimer(self, zoneId, delay, callback) local handle = Timers:CreateTimer( delay, function() if not self.spawnConfigs:has(zoneId) then return nil end return callback(nil) end ) local list = self.zoneTimerHandles:get(zoneId) if not list then list = {} self.zoneTimerHandles:set(zoneId, list) end list[#list + 1] = handle end function SpawnManager.prototype.cancelZoneTimers(self, zoneId) local list = self.zoneTimerHandles:get(zoneId) if not list then return end for ____, h in ipairs(list) do if h ~= nil and h ~= nil then Timers:RemoveTimer(h) end end self.zoneTimerHandles:delete(zoneId) end function SpawnManager.prototype.DrawSpawnZoneDebug(self, id, config) local function getZoneColor(____, zoneId) if __TS__StringIncludes(zoneId, "witch") then return Vector(255, 0, 255) end if __TS__StringIncludes(zoneId, "pigs") then return Vector(255, 200, 150) end if __TS__StringIncludes(zoneId, "sheep") then return Vector(255, 255, 255) end if __TS__StringIncludes(zoneId, "wolf") then return Vector(100, 100, 255) end if __TS__StringIncludes(zoneId, "skeletons") then return Vector(200, 200, 200) end return Vector(0, 255, 0) end local zoneColor = getZoneColor(nil, id) DebugDrawCircle( config.position, Vector(255, 255, 0), 255, 50, true, -1 ) if config.shape == "square" then local halfWidth = config.width ~= nil and config.width / 2 or config.radius local halfHeight = config.height ~= nil and config.height / 2 or config.radius local z = config.position.z local center = config.position local topLeft = Vector(center.x - halfWidth, center.y + halfHeight, z) local topRight = Vector(center.x + halfWidth, center.y + halfHeight, z) local bottomRight = Vector(center.x + halfWidth, center.y - halfHeight, z) local bottomLeft = Vector(center.x - halfWidth, center.y - halfHeight, z) DebugDrawLine( topLeft, topRight, zoneColor.x, zoneColor.y, zoneColor.z, true, -1 ) DebugDrawLine( topRight, bottomRight, zoneColor.x, zoneColor.y, zoneColor.z, true, -1 ) DebugDrawLine( bottomRight, bottomLeft, zoneColor.x, zoneColor.y, zoneColor.z, true, -1 ) DebugDrawLine( bottomLeft, topLeft, zoneColor.x, zoneColor.y, zoneColor.z, true, -1 ) else DebugDrawCircle( config.position, zoneColor, 100, config.radius, true, -1 ) end end function SpawnManager.prototype.isWithinZoneBounds(self, config, pos) local dx = pos.x - config.position.x local dy = pos.y - config.position.y if config.shape == "square" then local halfWidth = config.width ~= nil and config.width / 2 or config.radius local halfHeight = config.height ~= nil and config.height / 2 or config.radius return math.abs(dx) <= halfWidth and math.abs(dy) <= halfHeight end local distanceSquared = dx * dx + dy * dy return distanceSquared <= config.radius * config.radius end function SpawnManager.prototype.isWithinChaseRange(self, config, pos) local dx = pos.x - config.position.x local dy = pos.y - config.position.y local margin = ____exports.SpawnManager.CHASE_LEASH_MARGIN if config.shape == "square" then local halfWidth = (config.width ~= nil and config.width / 2 or config.radius) + margin local halfHeight = (config.height ~= nil and config.height / 2 or config.radius) + margin return math.abs(dx) <= halfWidth and math.abs(dy) <= halfHeight end local distanceSquared = dx * dx + dy * dy local maxRadius = config.radius + margin return distanceSquared <= maxRadius * maxRadius end function SpawnManager.prototype.getDeepPositionInZone(self, config) if config.shape == "square" then local halfWidth = (config.width ~= nil and config.width / 2 or config.radius) * 0.5 local halfHeight = (config.height ~= nil and config.height / 2 or config.radius) * 0.5 local offsetX = RandomFloat(-halfWidth, halfWidth) local offsetY = RandomFloat(-halfHeight, halfHeight) return GetGroundPosition( Vector(config.position.x + offsetX, config.position.y + offsetY, config.position.z), nil ) end local angle = RandomFloat(0, 2 * math.pi) local distance = RandomFloat(0, config.radius * 0.5) return GetGroundPosition( Vector( config.position.x + distance * math.cos(angle), config.position.y + distance * math.sin(angle), config.position.z ), nil ) end function SpawnManager.prototype.clampToZoneBounds(self, config, pos) local dx = pos.x - config.position.x local dy = pos.y - config.position.y if config.shape == "square" then local halfWidth = config.width ~= nil and config.width / 2 or config.radius local halfHeight = config.height ~= nil and config.height / 2 or config.radius local clampedX = math.max( -halfWidth, math.min(halfWidth, dx) ) local clampedY = math.max( -halfHeight, math.min(halfHeight, dy) ) return GetGroundPosition( Vector(config.position.x + clampedX, config.position.y + clampedY, pos.z), nil ) end local distanceSquared = dx * dx + dy * dy if distanceSquared <= config.radius * config.radius then return pos end local distance = math.sqrt(distanceSquared) or 1 local scale = config.radius / distance return GetGroundPosition( Vector(config.position.x + dx * scale, config.position.y + dy * scale, pos.z), nil ) end function SpawnManager.prototype.getGroupForUnit(self, unit) for ____, ____value in __TS__Iterator(self.unitGroups) do local arr = ____value[2] if __TS__ArraySome( arr, function(____, u) return u == unit end ) then return arr end end return nil end function SpawnManager.prototype.removeUnitFromGroup(self, unit) for ____, ____value in __TS__Iterator(self.unitGroups) do local groupId = ____value[1] local arr = ____value[2] local idx = __TS__ArrayIndexOf(arr, unit) if idx >= 0 then __TS__ArraySplice(arr, idx, 1) if #arr == 0 then self.unitGroups:delete(groupId) end return end end end function SpawnManager.prototype.findOrCreateGroupForSpawn(self, zoneId, config, groupKey) local groupSize = config.moveInGroups.groupSize local prefix = ((zoneId .. "_") .. groupKey) .. "_grp_" local maxIndex = -1 for ____, ____value in __TS__Iterator(self.unitGroups) do local gid = ____value[1] local arr = ____value[2] do if not __TS__StringStartsWith(gid, prefix) then goto __continue109 end local alive = __TS__ArrayFilter( arr, function(____, u) return u and IsValidEntity(u) and u:IsAlive() end ) if #alive < groupSize then local center = self:getGroupCenter(alive) return {groupId = gid, spawnNear = center} end local num = __TS__ParseInt( __TS__StringReplace(gid, prefix, ""), 10 ) if not __TS__NumberIsNaN(__TS__Number(num)) and num > maxIndex then maxIndex = num end end ::__continue109:: end local newId = prefix .. tostring(maxIndex + 1) self.unitGroups:set(newId, {}) return {groupId = newId} end function SpawnManager.prototype.getGroupCenter(self, units) local alive = __TS__ArrayFilter( units, function(____, u) return u and IsValidEntity(u) and u:IsAlive() end ) if #alive == 0 then return Vector(0, 0, 0) end local x = 0 local y = 0 local z = 0 for ____, u in ipairs(alive) do local p = u:GetAbsOrigin() x = x + p.x y = y + p.y z = z + p.z end local n = #alive return Vector(x / n, y / n, z / n) end function SpawnManager.prototype.getGroupLeader(self, units) local alive = __TS__ArrayFilter( units, function(____, u) return u and IsValidEntity(u) and u:IsAlive() end ) return alive[1] end function SpawnManager.prototype.isSpawnAllowed(self) return GameRules:State_Get() == DOTA_GAMERULES_STATE_GAME_IN_PROGRESS end function SpawnManager.prototype.RemoveZoneAtPosition(self, pos) for ____, ____value in __TS__Iterator(self.spawnConfigs) do local id = ____value[1] local config = ____value[2] if self:isWithinZoneBounds(config, pos) then self:RemoveSpawnZone(id) return id end end return nil end function SpawnManager.prototype.SpawnInitialUnits(self, zoneId, config) if not self:isSpawnAllowed() then self:startZoneTimer( zoneId, 0.5, function() self:SpawnInitialUnits(zoneId, config) return nil end ) return end __TS__ArrayForEach( config.units, function(____, unitInfo) do local i = 0 while i < unitInfo.count do self:startZoneTimer( zoneId, i * 0.1, function() self:SpawnUnit(zoneId, config, unitInfo) return nil end ) i = i + 1 end end end ) end function SpawnManager.prototype.SpawnUnit(self, id, config, unitInfo) if not self.spawnConfigs:has(id) then return end local unitName = unitInfo.unitName local units = self.spawnedUnits:get(id) local aliveUnits = __TS__ArrayFilter( units, function(____, unit) return unit ~= nil and unit:IsAlive() end ) local currentTypeCount = #__TS__ArrayFilter( aliveUnits, function(____, unit) do local function ____catch(____error) return true, false end local ____try, ____hasReturned, ____returnValue = pcall(function() return true, unit and IsValidEntity(unit) and self:getSpawnUnitConfigName(unit) == unitName end) if not ____try then ____hasReturned, ____returnValue = ____catch(____hasReturned) end if ____hasReturned then return ____returnValue end end end ) if currentTypeCount >= unitInfo.count then return end local totalMaxUnits = __TS__ArrayReduce( config.units, function(____, sum, unit) return sum + unit.count end, 0 ) if #aliveUnits >= totalMaxUnits then return end local groupInfo if config.moveInGroups and unitInfo.groupKey then groupInfo = self:findOrCreateGroupForSpawn(id, config, unitInfo.groupKey) end local function isWithinRadius(____, pos) return self:isWithinZoneBounds(config, pos) end local spawnPos local attempts = 0 local maxAttempts = 20 while not spawnPos and attempts < maxAttempts do attempts = attempts + 1 local testPos if config.moveInGroups and (groupInfo and groupInfo.spawnNear) and attempts <= 5 then local r = config.moveInGroups.stayRadius or 250 testPos = GetGroundPosition( Vector( groupInfo.spawnNear.x + RandomFloat(-r, r), groupInfo.spawnNear.y + RandomFloat(-r, r), groupInfo.spawnNear.z ), nil ) elseif config.shape == "square" then local halfWidth = config.width ~= nil and config.width / 2 or config.radius local halfHeight = config.height ~= nil and config.height / 2 or config.radius local offsetX = RandomFloat(-halfWidth, halfWidth) local offsetY = RandomFloat(-halfHeight, halfHeight) testPos = GetGroundPosition( Vector(config.position.x + offsetX, config.position.y + offsetY, config.position.z), nil ) else local angle = RandomFloat(0, 2 * math.pi) local distance = RandomFloat(0, config.radius) testPos = GetGroundPosition( Vector( config.position.x + distance * math.cos(angle), config.position.y + distance * math.sin(angle), config.position.z ), nil ) end if isWithinRadius(nil, testPos) then spawnPos = testPos break end end if not spawnPos then spawnPos = config.position end local unit = CreateUnitByName( unitName, spawnPos, true, nil, nil, config.team ) if unit and IsValidEntity(unit) then unit:SetEntityName((unitInfo.unitName .. "_") .. tostring(unit:entindex())) unit.__spawnManagerUnitName = unitInfo.unitName if config.moveInGroups and groupInfo then local ____temp_3 = self.unitGroups:get(groupInfo.groupId) ____temp_3[#____temp_3 + 1] = unit end if unit:IsAlive() then repeat local ____switch153 = config.behavior local friendlySpawnPosition, checkPosition, aggressiveSpawnPosition, canUseAbilities, ignoreAiModifiers, aggressiveLastAbilityUseTime, aggressiveIsApproachingForAbility, aggressiveIsCastingAbility, aggressiveCastingEndTime, aggressiveLastPos, aggressiveLastPosCheckTime, aggressiveStuckCounter, getKiteRetreatDistance, hasReadyAbilities, tryUseAbilitiesAggressive, checkAggressivePosition local ____cond153 = ____switch153 == "friendly" if ____cond153 then unit:SetAttackCapability(DOTA_UNIT_CAP_NO_ATTACK) friendlySpawnPosition = unit:GetAbsOrigin() checkPosition = function() if not unit or not unit:IsAlive() then return nil end local currentPos = unit:GetAbsOrigin() if not self:isWithinZoneBounds(config, currentPos) then local zoneCenter = config.position ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = zoneCenter, Queue = false }) return 1 end return 1 end self:startZoneTimer(id, 1, checkPosition) break end ____cond153 = ____cond153 or ____switch153 == "aggressive" if ____cond153 then aggressiveSpawnPosition = unit:GetAbsOrigin() local ____unitInfo_canUseAbilities_4 = unitInfo.canUseAbilities if ____unitInfo_canUseAbilities_4 == nil then ____unitInfo_canUseAbilities_4 = false end canUseAbilities = ____unitInfo_canUseAbilities_4 ignoreAiModifiers = unitInfo.ignoreAIWhileHasModifiers or ({}) aggressiveLastAbilityUseTime = 0 aggressiveIsApproachingForAbility = false aggressiveIsCastingAbility = false aggressiveCastingEndTime = 0 aggressiveLastPos = unit:GetAbsOrigin() aggressiveLastPosCheckTime = GameRules:GetGameTime() aggressiveStuckCounter = 0 getKiteRetreatDistance = function() return unitInfo.kiteRetreatDistance end hasReadyAbilities = function(____, target) if not canUseAbilities then return false end if not unit or not unit:IsAlive() or unit:IsStunned() or unit:IsHexed() then return false end local now = GameRules:GetGameTime() if now - aggressiveLastAbilityUseTime < 2 then return false end do local i = 0 while i < unit:GetAbilityCount() do do local ability = unit:GetAbilityByIndex(i) if not ability or ability:IsNull() then goto __continue162 end if ability:IsPassive() or ability:IsHidden() then goto __continue162 end if not ability:IsCooldownReady() then goto __continue162 end if ability:GetManaCost(ability:GetLevel()) > unit:GetMana() then goto __continue162 end return true end ::__continue162:: i = i + 1 end end return false end tryUseAbilitiesAggressive = function() if #ignoreAiModifiers > 0 then for ____, mod in ipairs(ignoreAiModifiers) do if unit:HasModifier(mod) then return false end end end if not canUseAbilities then return false end if not unit or not unit:IsAlive() or unit:IsStunned() or unit:IsHexed() then return false end local now = GameRules:GetGameTime() if now - aggressiveLastAbilityUseTime < 2 then return false end local enemiesRaw = FindUnitsInRadius( unit:GetTeamNumber(), unit:GetAbsOrigin(), nil, 1500, DOTA_UNIT_TARGET_TEAM_ENEMY, bit.bor(DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_BASIC), DOTA_UNIT_TARGET_FLAG_NONE, FIND_CLOSEST, false ) local enemies = __TS__ArrayFilter( enemiesRaw, function(____, e) return self:isWithinChaseRange( config, e:GetAbsOrigin() ) end ) if #enemies == 0 then return false end local target = enemies[1] do local i = 0 while i < unit:GetAbilityCount() do do local ability = unit:GetAbilityByIndex(i) if not ability or ability:IsNull() then goto __continue177 end local abilityName = ability:GetAbilityName() local suppressWarningParticles = abilityName == "spider_hunger" if ability:IsPassive() or ability:IsHidden() then goto __continue177 end if not ability:IsCooldownReady() then goto __continue177 end local manaCost = ability:GetManaCost(ability:GetLevel()) local currentMana = unit:GetMana() if manaCost > currentMana then goto __continue177 end local behavior = 0 do local function ____catch(e) local abilityName = ability:GetAbilityName() if abilityName == "sheep_coil" then behavior = 16 elseif abilityName == "black_dragon_fireball" then behavior = 16 elseif abilityName == "frogmen_acid_jump" then behavior = 16 else local behaviorRaw = ability:GetBehavior() if type(behaviorRaw) == "number" and not __TS__NumberIsNaN(__TS__Number(behaviorRaw)) then behavior = behaviorRaw end end end local ____try, ____hasReturned = pcall(function() local kv = ability:GetAbilityKeyValues() if kv and kv.AbilityBehavior then local behaviorStr = kv.AbilityBehavior if __TS__StringIncludes(behaviorStr, "POINT") then behavior = bit.bor(behavior, 16) end if __TS__StringIncludes(behaviorStr, "UNIT_TARGET") then behavior = bit.bor(behavior, 32) end if __TS__StringIncludes(behaviorStr, "NO_TARGET") then behavior = bit.bor(behavior, 4) end end end) if not ____try then ____catch(____hasReturned) end end local pointFlag = 16 local unitTargetFlag = 32 local noTargetFlag = 4 local unitPos = unit:GetAbsOrigin() local castRange = ability:GetCastRange(unitPos, target) local targetPos = target:GetAbsOrigin() local dx = unitPos.x - targetPos.x local dy = unitPos.y - targetPos.y local distance = math.sqrt(dx * dx + dy * dy) local ____table_GetSpecialValueFor_5 if ability.GetSpecialValueFor then ____table_GetSpecialValueFor_5 = ability:GetSpecialValueFor("precast_warning_time") or 1 else ____table_GetSpecialValueFor_5 = 1 end local warningTime = ____table_GetSpecialValueFor_5 local isThiefUnit = (function() if not unit or unit:IsNull() then return false end local name = unit:GetUnitName() return name == "npc_thief_archer" or name == "npc_thief_leader" end)(nil) local function createPreCastParticle(____, particlePosition, radius, warningDuration) if isThiefUnit then return end local duration = warningDuration ~= nil and warningDuration or 1 local groundPos = GetGroundPosition(particlePosition, nil) local aoeRadius = radius ~= nil and radius or 0 if aoeRadius > 0 then local particle = ParticleManager:CreateParticle("particles/econ/events/darkmoon_2017/darkmoon_generic_aoe.vpcf", PATTACH_CUSTOMORIGIN, nil) if particle ~= nil then ParticleManager:SetParticleControl(particle, 0, groundPos) ParticleManager:SetParticleControl( particle, 1, Vector(aoeRadius, 0, 0) ) ParticleManager:SetParticleControl( particle, 2, Vector(6, 0, 1) ) ParticleManager:SetParticleControl( particle, 3, Vector(200, 0, 0) ) ParticleManager:SetParticleControl(particle, 4, groundPos) self:startZoneTimer( id, duration, function() if particle ~= nil then ParticleManager:DestroyParticle(particle, false) ParticleManager:ReleaseParticleIndex(particle) end return nil end ) end else local casterPos = GetGroundPosition( unit:GetAbsOrigin(), nil ) local targetPos = groundPos local particle = ParticleManager:CreateParticle("particles/econ/events/darkmoon_2017/darkmoon_calldown_marker_arrows.vpcf", PATTACH_CUSTOMORIGIN, nil) if particle ~= nil then ParticleManager:SetParticleControl(particle, 0, casterPos) ParticleManager:SetParticleControl(particle, 1, targetPos) ParticleManager:SetParticleControl( particle, 2, Vector(duration, 0, 0) ) self:startZoneTimer( id, duration, function() if particle ~= nil then ParticleManager:DestroyParticle(particle, false) ParticleManager:ReleaseParticleIndex(particle) end return nil end ) end end end local function createDirectionArrow(____, caster, targetPosition, warningDuration) if isThiefUnit then return end local duration = warningDuration ~= nil and warningDuration or 1 local casterPos = GetGroundPosition( caster:GetAbsOrigin(), nil ) local groundTarget = GetGroundPosition(targetPosition, nil) local particle = ParticleManager:CreateParticle("particles/econ/events/darkmoon_2017/darkmoon_calldown_marker_arrows.vpcf", PATTACH_CUSTOMORIGIN, nil) if particle ~= nil then ParticleManager:SetParticleControl(particle, 0, casterPos) ParticleManager:SetParticleControl(particle, 1, groundTarget) ParticleManager:SetParticleControl( particle, 2, Vector(duration, 0, 0) ) self:startZoneTimer( id, duration, function() if particle ~= nil then ParticleManager:DestroyParticle(particle, false) ParticleManager:ReleaseParticleIndex(particle) end return nil end ) end end if bit.band(behavior, pointFlag) ~= 0 then if distance <= castRange and target:IsAlive() then if abilityName ~= "frog_magi_wave" and not suppressWarningParticles then local ____table_GetAOERadius_6 if ability.GetAOERadius then ____table_GetAOERadius_6 = ability:GetAOERadius() else ____table_GetAOERadius_6 = 0 end local aoeRadius = ____table_GetAOERadius_6 local ____temp_7 if aoeRadius > 0 then ____temp_7 = aoeRadius else ____temp_7 = 200 end createPreCastParticle(nil, targetPos, ____temp_7, warningTime) end local direction = Vector(targetPos.x - unitPos.x, targetPos.y - unitPos.y, 0) local length = math.sqrt(direction.x * direction.x + direction.y * direction.y) if length > 0 then local normalizedDir = Vector(direction.x / length, direction.y / length, 0) unit:SetForwardVector(normalizedDir) end aggressiveIsCastingAbility = true aggressiveCastingEndTime = now + warningTime + (ability:GetCastPoint() or 0) self:startZoneTimer( id, warningTime, function() if not unit or not unit:IsAlive() or not target or not target:IsAlive() then return nil end ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_CAST_POSITION, Position = targetPos, AbilityIndex = ability:GetEntityIndex(), Queue = false }) local castPoint = ability:GetCastPoint() or 0 self:startZoneTimer( id, castPoint, function() aggressiveIsCastingAbility = false return nil end ) return nil end ) aggressiveLastAbilityUseTime = now aggressiveIsApproachingForAbility = false return true end elseif bit.band(behavior, unitTargetFlag) ~= 0 then if distance <= castRange and target:IsAlive() then if not suppressWarningParticles then local overheadParticle = ParticleManager:CreateParticle("particles/econ/items/faceless_void/faceless_void_arcana/faceless_void_arcana_deny_v2_symbol_question.vpcf", PATTACH_OVERHEAD_FOLLOW, target) if overheadParticle ~= nil then self:startZoneTimer( id, warningTime + 0.5, function() if overheadParticle ~= nil then ParticleManager:DestroyParticle(overheadParticle, false) ParticleManager:ReleaseParticleIndex(overheadParticle) end return nil end ) end end local direction = Vector(targetPos.x - unitPos.x, targetPos.y - unitPos.y, 0) local length = math.sqrt(direction.x * direction.x + direction.y * direction.y) if length > 0 then local normalizedDir = Vector(direction.x / length, direction.y / length, 0) unit:SetForwardVector(normalizedDir) end aggressiveIsCastingAbility = true aggressiveCastingEndTime = now + warningTime + (ability:GetCastPoint() or 0) self:startZoneTimer( id, warningTime, function() if not unit or not unit:IsAlive() or not target or not target:IsAlive() then return nil end ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_CAST_TARGET, TargetIndex = target:entindex(), AbilityIndex = ability:GetEntityIndex(), Queue = false }) local castPoint = ability:GetCastPoint() or 0 self:startZoneTimer( id, castPoint, function() aggressiveIsCastingAbility = false return nil end ) return nil end ) aggressiveLastAbilityUseTime = now aggressiveIsApproachingForAbility = false return true end elseif bit.band(behavior, noTargetFlag) ~= 0 then local lockedTargetPos = target:GetAbsOrigin() local ____table_GetAOERadius_8 if ability.GetAOERadius then ____table_GetAOERadius_8 = ability:GetAOERadius() else ____table_GetAOERadius_8 = 0 end local aoeRadius = ____table_GetAOERadius_8 if aoeRadius <= 0 and ability.GetSpecialValueFor then local specialRadius = ability:GetSpecialValueFor("radius") if specialRadius and specialRadius > 0 then aoeRadius = specialRadius end end if not suppressWarningParticles then local ____temp_9 if aoeRadius > 0 then ____temp_9 = aoeRadius else ____temp_9 = 200 end local followRadius = ____temp_9 local followParticle = ParticleManager:CreateParticle("particles/econ/events/darkmoon_2017/darkmoon_generic_aoe.vpcf", PATTACH_CUSTOMORIGIN, nil) if followParticle ~= nil then ParticleManager:SetParticleControlEnt( followParticle, 0, unit, PATTACH_ABSORIGIN_FOLLOW, nil, unit:GetAbsOrigin(), true ) ParticleManager:SetParticleControl( followParticle, 1, Vector(followRadius, 0, 0) ) ParticleManager:SetParticleControl( followParticle, 2, Vector(6, 0, 1) ) ParticleManager:SetParticleControl( followParticle, 3, Vector(200, 0, 0) ) ParticleManager:SetParticleControl( followParticle, 4, unit:GetAbsOrigin() ) self:startZoneTimer( id, warningTime, function() if followParticle ~= nil then ParticleManager:DestroyParticle(followParticle, false) ParticleManager:ReleaseParticleIndex(followParticle) end return nil end ) end end do local direction = Vector(lockedTargetPos.x - unitPos.x, lockedTargetPos.y - unitPos.y, 0) local length = math.sqrt(direction.x * direction.x + direction.y * direction.y) if length > 0 then local normalizedDir = Vector(direction.x / length, direction.y / length, 0) unit:SetForwardVector(normalizedDir) end end if not suppressWarningParticles then createDirectionArrow(nil, unit, lockedTargetPos, warningTime) end self:startZoneTimer( id, warningTime, function() if not unit or not unit:IsAlive() then return nil end local currentPos = unit:GetAbsOrigin() local direction = Vector(lockedTargetPos.x - currentPos.x, lockedTargetPos.y - currentPos.y, 0) local length = math.sqrt(direction.x * direction.x + direction.y * direction.y) if length > 0 then local normalizedDir = Vector(direction.x / length, direction.y / length, 0) unit:SetForwardVector(normalizedDir) end aggressiveIsCastingAbility = true aggressiveCastingEndTime = now + warningTime + (ability:GetCastPoint() or 0) ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_CAST_NO_TARGET, AbilityIndex = ability:GetEntityIndex(), Queue = false }) local castPoint = ability:GetCastPoint() or 0 self:startZoneTimer( id, castPoint, function() aggressiveIsCastingAbility = false return nil end ) return nil end ) aggressiveLastAbilityUseTime = now aggressiveIsApproachingForAbility = false return true end end ::__continue177:: i = i + 1 end end return false end checkAggressivePosition = function() if not unit or not unit:IsAlive() then return nil end local currentHealth = unit:GetHealth() local maxHealth = unit:GetMaxHealth() local currentPos = unit:GetAbsOrigin() local now = GameRules:GetGameTime() if #ignoreAiModifiers > 0 then for ____, mod in ipairs(ignoreAiModifiers) do if unit:HasModifier(mod) then return 0.2 end end end if aggressiveIsCastingAbility then if now < aggressiveCastingEndTime then return 0.2 else aggressiveIsCastingAbility = false end end if config.moveInGroups then local group = self:getGroupForUnit(unit) if group and #group > 0 then local leader = self:getGroupLeader(group) if leader and leader ~= unit and leader:IsAlive() then local leaderPos = leader:GetAbsOrigin() local followRadius = config.moveInGroups.stayRadius or 250 local dxF = currentPos.x - leaderPos.x local dyF = currentPos.y - leaderPos.y local distToLeader = math.sqrt(dxF * dxF + dyF * dyF) if distToLeader > followRadius * 0.7 and not unit:IsAttacking() and not unit:IsChanneling() then ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = leaderPos, Queue = false }) return 0.2 end end end end if aggressiveLastPos then local dt = now - aggressiveLastPosCheckTime if dt >= 1 then local dxStuck = currentPos.x - aggressiveLastPos.x local dyStuck = currentPos.y - aggressiveLastPos.y local distMoved = math.sqrt(dxStuck * dxStuck + dyStuck * dyStuck) if distMoved < 10 and not unit:IsChanneling() and not unit:IsAttacking() then aggressiveStuckCounter = aggressiveStuckCounter + 1 if aggressiveStuckCounter >= 2 then local escapePos if config.shape == "square" then local halfWidth = config.width ~= nil and config.width / 2 or config.radius local halfHeight = config.height ~= nil and config.height / 2 or config.radius local offsetX = RandomFloat(-halfWidth, halfWidth) local offsetY = RandomFloat(-halfHeight, halfHeight) escapePos = GetGroundPosition( Vector(config.position.x + offsetX, config.position.y + offsetY, config.position.z), nil ) else local angle = RandomFloat(0, 2 * math.pi) local distance = RandomFloat(0, config.radius) escapePos = GetGroundPosition( Vector( config.position.x + distance * math.cos(angle), config.position.y + distance * math.sin(angle), config.position.z ), nil ) end ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = escapePos, Queue = false }) aggressiveStuckCounter = 0 end else aggressiveStuckCounter = 0 end aggressiveLastPos = currentPos aggressiveLastPosCheckTime = now end else aggressiveLastPos = currentPos aggressiveLastPosCheckTime = now end local isOutsideZone = not self:isWithinZoneBounds(config, currentPos) local lastAttackerIndex = unit:GetContext("last_attacker") local lastAttacker if lastAttackerIndex and lastAttackerIndex > 0 then lastAttacker = EntIndexToHScript(lastAttackerIndex) if not lastAttacker or not IsValidEntity(lastAttacker) or not lastAttacker:IsAlive() then lastAttacker = nil elseif not self:isWithinChaseRange( config, lastAttacker:GetAbsOrigin() ) then lastAttacker = nil end end if lastAttacker and lastAttacker:IsAlive() then local unitPos = unit:GetAbsOrigin() local attackerPos = lastAttacker:GetAbsOrigin() local dx = unitPos.x - attackerPos.x local dy = unitPos.y - attackerPos.y local distanceToAttacker = math.sqrt(dx * dx + dy * dy) local hasAbilities = hasReadyAbilities(nil, lastAttacker) if hasAbilities then local used = tryUseAbilitiesAggressive(nil) if used then return 0.5 end end local retreatDistance = getKiteRetreatDistance(nil) if not hasAbilities and retreatDistance == nil and distanceToAttacker < 140 then local attackTarget = unit:GetAttackTarget() local isAttacking = unit:IsAttacking() local isInAttackRange = attackTarget == lastAttacker and isAttacking and distanceToAttacker <= unit:GetBaseAttackRange() * 1.2 if isInAttackRange then return 0.5 end end if hasAbilities then local maxCastRange = 0 local hasReadyAbility = false do local i = 0 while i < unit:GetAbilityCount() do do local ability = unit:GetAbilityByIndex(i) if not ability or ability:IsNull() then goto __continue271 end if ability:IsPassive() or ability:IsHidden() then goto __continue271 end if not ability:IsCooldownReady() then goto __continue271 end local manaCost = ability:GetManaCost(ability:GetLevel()) if manaCost > unit:GetMana() then goto __continue271 end hasReadyAbility = true local castRange = ability:GetCastRange(unitPos, lastAttacker) if castRange > maxCastRange then maxCastRange = castRange end end ::__continue271:: i = i + 1 end end if hasReadyAbility and distanceToAttacker > maxCastRange and maxCastRange > 0 then aggressiveIsApproachingForAbility = true ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = attackerPos, Queue = false }) self:startZoneTimer( id, 3, function() aggressiveIsApproachingForAbility = false return nil end ) return 0.5 elseif hasReadyAbility and distanceToAttacker <= maxCastRange then aggressiveIsApproachingForAbility = false if retreatDistance ~= nil and distanceToAttacker < retreatDistance then local dir = Vector(unitPos.x - attackerPos.x, unitPos.y - attackerPos.y, 0) local len = math.sqrt(dir.x * dir.x + dir.y * dir.y) or 1 local normalizedDir = Vector(dir.x / len, dir.y / len, 0) local retreatPos = Vector(attackerPos.x + normalizedDir.x * retreatDistance, attackerPos.y + normalizedDir.y * retreatDistance, attackerPos.z) local ____temp_10 if self:isWithinZoneBounds(config, retreatPos) and GridNav.CanFindPath then ____temp_10 = GridNav:CanFindPath(unitPos, retreatPos) else ____temp_10 = true end local canEscape = ____temp_10 if canEscape then print(((((((((unit:GetUnitName() .. " kites (") .. tostring(math.floor(attackerPos.x))) .. ",") .. tostring(math.floor(attackerPos.y))) .. ") -> (") .. tostring(math.floor(retreatPos.x))) .. ",") .. tostring(math.floor(retreatPos.y))) .. ") [abilities]") ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = retreatPos, Queue = false }) else print(unit:GetUnitName() .. " cannot escape while kiting (no path / out of zone), fights back attacker") ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET, TargetIndex = lastAttacker:entindex(), Queue = false }) end else ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET, TargetIndex = lastAttacker:entindex(), Queue = false }) end return 0.5 else aggressiveIsApproachingForAbility = false if retreatDistance ~= nil and distanceToAttacker < retreatDistance then local dir = Vector(unitPos.x - attackerPos.x, unitPos.y - attackerPos.y, 0) local len = math.sqrt(dir.x * dir.x + dir.y * dir.y) or 1 local normalizedDir = Vector(dir.x / len, dir.y / len, 0) local retreatPos = Vector(attackerPos.x + normalizedDir.x * retreatDistance, attackerPos.y + normalizedDir.y * retreatDistance, attackerPos.z) local ____temp_11 if self:isWithinZoneBounds(config, retreatPos) and GridNav.CanFindPath then ____temp_11 = GridNav:CanFindPath(unitPos, retreatPos) else ____temp_11 = true end local canEscape = ____temp_11 if canEscape then print(((((((((unit:GetUnitName() .. " kites (") .. tostring(math.floor(attackerPos.x))) .. ",") .. tostring(math.floor(attackerPos.y))) .. ") -> (") .. tostring(math.floor(retreatPos.x))) .. ",") .. tostring(math.floor(retreatPos.y))) .. ") [no_abilities]") ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = retreatPos, Queue = false }) else print(unit:GetUnitName() .. " cannot escape while kiting without abilities, fights back attacker") ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET, TargetIndex = lastAttacker:entindex(), Queue = false }) end else ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET, TargetIndex = lastAttacker:entindex(), Queue = false }) end return 0.5 end end end if not aggressiveIsApproachingForAbility then local detectionRadius = 900 local nearbyEnemiesRaw = FindUnitsInRadius( unit:GetTeamNumber(), unit:GetAbsOrigin(), nil, detectionRadius, DOTA_UNIT_TARGET_TEAM_ENEMY, bit.bor(DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_BASIC), DOTA_UNIT_TARGET_FLAG_NONE, FIND_CLOSEST, false ) local nearbyEnemies = __TS__ArrayFilter( nearbyEnemiesRaw, function(____, e) return self:isWithinChaseRange( config, e:GetAbsOrigin() ) end ) if #nearbyEnemies > 0 then local closestEnemy = nearbyEnemies[1] local unitPos = unit:GetAbsOrigin() local enemyPos = closestEnemy:GetAbsOrigin() local dx = unitPos.x - enemyPos.x local dy = unitPos.y - enemyPos.y local distanceToEnemy = math.sqrt(dx * dx + dy * dy) if distanceToEnemy <= detectionRadius and closestEnemy:IsAlive() then local retreatDistance = getKiteRetreatDistance(nil) if retreatDistance ~= nil and distanceToEnemy < retreatDistance then local dir = Vector(unitPos.x - enemyPos.x, unitPos.y - enemyPos.y, 0) local len = math.sqrt(dir.x * dir.x + dir.y * dir.y) or 1 local normalizedDir = Vector(dir.x / len, dir.y / len, 0) local retreatPos = Vector(enemyPos.x + normalizedDir.x * retreatDistance, enemyPos.y + normalizedDir.y * retreatDistance, enemyPos.z) local ____temp_12 if self:isWithinZoneBounds(config, retreatPos) and GridNav.CanFindPath then ____temp_12 = GridNav:CanFindPath(unitPos, retreatPos) else ____temp_12 = true end local canEscape = ____temp_12 if canEscape then print(((((((((unit:GetUnitName() .. " kites (") .. tostring(math.floor(enemyPos.x))) .. ",") .. tostring(math.floor(enemyPos.y))) .. ") -> (") .. tostring(math.floor(retreatPos.x))) .. ",") .. tostring(math.floor(retreatPos.y))) .. ") [search]") ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = retreatPos, Queue = false }) else print(unit:GetUnitName() .. " cannot escape while kiting in search phase, attacks closest enemy") ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET, TargetIndex = closestEnemy:entindex(), Queue = false }) end else ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET, TargetIndex = closestEnemy:entindex(), Queue = false }) end if distanceToEnemy <= 800 then local usedAbility = tryUseAbilitiesAggressive(nil) if usedAbility then return 0.5 end end end end end local nearbyEnemiesCheckRaw = FindUnitsInRadius( unit:GetTeamNumber(), unit:GetAbsOrigin(), nil, 1000, DOTA_UNIT_TARGET_TEAM_ENEMY, bit.bor(DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_BASIC), DOTA_UNIT_TARGET_FLAG_NONE, FIND_CLOSEST, false ) local nearbyEnemiesCheck = __TS__ArrayFilter( nearbyEnemiesCheckRaw, function(____, e) return self:isWithinChaseRange( config, e:GetAbsOrigin() ) end ) local hasReadyAbilitiesForAnyEnemy = false if #nearbyEnemiesCheck > 0 and canUseAbilities then for ____, enemy in ipairs(nearbyEnemiesCheck) do if hasReadyAbilities(nil, enemy) then hasReadyAbilitiesForAnyEnemy = true break end end end if currentHealth >= maxHealth * 0.95 and not aggressiveIsApproachingForAbility and #nearbyEnemiesCheck == 0 and not hasReadyAbilitiesForAnyEnemy then local currentPos2 = unit:GetAbsOrigin() local returnTarget = config.moveInGroups and self:getGroupForUnit(unit) and self:getGroupCenter(self:getGroupForUnit(unit)) or aggressiveSpawnPosition local threshold = config.moveInGroups and (config.moveInGroups.stayRadius or 250) or 200 local dx = currentPos2.x - returnTarget.x local dy = currentPos2.y - returnTarget.y local distanceToSpawn = math.sqrt(dx * dx + dy * dy) if distanceToSpawn > threshold then ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = returnTarget, Queue = false }) end end if isOutsideZone and #nearbyEnemiesCheck == 0 and not hasReadyAbilitiesForAnyEnemy and not aggressiveIsApproachingForAbility then local deepPos = self:getDeepPositionInZone(config) ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = deepPos, Queue = false }) self:startZoneTimer( id, 1, function() if not unit or not unit:IsAlive() then return nil end local checkPos = unit:GetAbsOrigin() if not self:isWithinZoneBounds(config, checkPos) then ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = self:getDeepPositionInZone(config), Queue = false }) end return nil end ) return 0.5 end return 0.5 end self:startZoneTimer(id, 1, checkAggressivePosition) break end ____cond153 = ____cond153 or ____switch153 == "neutral" if ____cond153 then do unit:SetAttackCapability(DOTA_UNIT_CAP_NO_ATTACK) local spawnPosition = unit:GetAbsOrigin() local ____unitInfo_canUseAbilities_13 = unitInfo.canUseAbilities if ____unitInfo_canUseAbilities_13 == nil then ____unitInfo_canUseAbilities_13 = false end local neutralCanUseAbilities = ____unitInfo_canUseAbilities_13 local isAggressive = false local neutralLastAbilityUseTime = 0 local checkAggression local function hasReadyAbilitiesNeutral(____, target) if not neutralCanUseAbilities then return false end if not unit or not unit:IsAlive() or unit:IsStunned() or unit:IsHexed() then return false end local now = GameRules:GetGameTime() if now - neutralLastAbilityUseTime < 2 then return false end local unitPos = unit:GetAbsOrigin() do local i = 0 while i < unit:GetAbilityCount() do do local ability = unit:GetAbilityByIndex(i) if not ability or ability:IsNull() then goto __continue315 end if ability:IsPassive() or ability:IsHidden() then goto __continue315 end if not ability:IsCooldownReady() then goto __continue315 end if ability:GetManaCost(ability:GetLevel()) > unit:GetMana() then goto __continue315 end local behavior = 0 do local function ____catch(e) local abilityNameForCheck = ability:GetAbilityName() if abilityNameForCheck == "sheep_coil" then behavior = 16 elseif abilityNameForCheck == "black_dragon_fireball" then behavior = 16 elseif abilityNameForCheck == "frogmen_acid_jump" then behavior = 16 else local behaviorRaw = ability:GetBehavior() if type(behaviorRaw) == "number" and not __TS__NumberIsNaN(__TS__Number(behaviorRaw)) then behavior = behaviorRaw end end end local ____try, ____hasReturned = pcall(function() local kv = ability:GetAbilityKeyValues() if kv and kv.AbilityBehavior then local behaviorStr = kv.AbilityBehavior if __TS__StringIncludes(behaviorStr, "POINT") then behavior = bit.bor(behavior, 16) end if __TS__StringIncludes(behaviorStr, "UNIT_TARGET") then behavior = bit.bor(behavior, 32) end if __TS__StringIncludes(behaviorStr, "NO_TARGET") then behavior = bit.bor(behavior, 4) end end end) if not ____try then ____catch(____hasReturned) end end local pointFlag = 16 local unitTargetFlag = 32 local noTargetFlag = 4 local castRange = ability:GetCastRange(unitPos, target) local targetPos = target:GetAbsOrigin() local dx = unitPos.x - targetPos.x local dy = unitPos.y - targetPos.y local distance = math.sqrt(dx * dx + dy * dy) if bit.band(behavior, unitTargetFlag) ~= 0 or bit.band(behavior, pointFlag) ~= 0 then if distance <= castRange and target:IsAlive() then return true end elseif bit.band(behavior, noTargetFlag) ~= 0 then return true end end ::__continue315:: i = i + 1 end end return false end local function tryUseAbilitiesNeutral() if not neutralCanUseAbilities then return false end if not unit or not unit:IsAlive() or unit:IsStunned() or unit:IsHexed() then return false end local now = GameRules:GetGameTime() if now - neutralLastAbilityUseTime < 2 then return false end local enemiesRaw = FindUnitsInRadius( unit:GetTeamNumber(), unit:GetAbsOrigin(), nil, 600, DOTA_UNIT_TARGET_TEAM_ENEMY, bit.bor(DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_BASIC), DOTA_UNIT_TARGET_FLAG_NONE, FIND_CLOSEST, false ) local enemies = __TS__ArrayFilter( enemiesRaw, function(____, e) return self:isWithinChaseRange( config, e:GetAbsOrigin() ) end ) if #enemies == 0 then return false end local target = enemies[1] do local i = 0 while i < unit:GetAbilityCount() do do local ability = unit:GetAbilityByIndex(i) if not ability or ability:IsNull() then goto __continue340 end local abilityName = ability:GetAbilityName() local suppressWarningParticles = abilityName == "spider_hunger" if ability:IsPassive() or ability:IsHidden() then goto __continue340 end if not ability:IsCooldownReady() then goto __continue340 end if ability:GetManaCost(ability:GetLevel()) > unit:GetMana() then goto __continue340 end local pointFlag = 16 local unitTargetFlag = 32 local noTargetFlag = 4 local knownAbilityBehavior = {sheep_coil = pointFlag, black_dragon_fireball = pointFlag, frogmen_acid_jump = pointFlag} local behavior = 0 do local function ____catch(e) local abilityNameForCheck = ability:GetAbilityName() local override = knownAbilityBehavior[abilityNameForCheck] if override ~= nil then behavior = override else local behaviorRaw = ability:GetBehavior() if type(behaviorRaw) == "number" and not __TS__NumberIsNaN(__TS__Number(behaviorRaw)) then behavior = behaviorRaw end end end local ____try, ____hasReturned = pcall(function() local kv = ability:GetAbilityKeyValues() if kv and kv.AbilityBehavior then local behaviorStr = kv.AbilityBehavior if __TS__StringIncludes(behaviorStr, "POINT") then behavior = bit.bor(behavior, pointFlag) end if __TS__StringIncludes(behaviorStr, "UNIT_TARGET") then behavior = bit.bor(behavior, unitTargetFlag) end if __TS__StringIncludes(behaviorStr, "NO_TARGET") then behavior = bit.bor(behavior, noTargetFlag) end end end) if not ____try then ____catch(____hasReturned) end end local unitPos = unit:GetAbsOrigin() local castRange = ability:GetCastRange(unitPos, target) local ____table_GetSpecialValueFor_14 if ability.GetSpecialValueFor then ____table_GetSpecialValueFor_14 = ability:GetSpecialValueFor("precast_warning_time") or 1 else ____table_GetSpecialValueFor_14 = 1 end local warningTimeNeutral = ____table_GetSpecialValueFor_14 local function createPreCastParticle(____, particlePosition, radius, warningDuration) local duration = warningDuration ~= nil and warningDuration or warningTimeNeutral local groundPos = GetGroundPosition(particlePosition, nil) local aoeRadius = radius ~= nil and radius or 0 if aoeRadius > 0 then local particle = ParticleManager:CreateParticle("particles/econ/events/darkmoon_2017/darkmoon_generic_aoe.vpcf", PATTACH_CUSTOMORIGIN, nil) if particle ~= nil then ParticleManager:SetParticleControl(particle, 0, groundPos) ParticleManager:SetParticleControl( particle, 1, Vector(aoeRadius, 0, 0) ) ParticleManager:SetParticleControl( particle, 2, Vector(6, 0, 1) ) ParticleManager:SetParticleControl( particle, 3, Vector(200, 0, 0) ) ParticleManager:SetParticleControl(particle, 4, groundPos) self:startZoneTimer( id, duration, function() if particle ~= nil then ParticleManager:DestroyParticle(particle, false) ParticleManager:ReleaseParticleIndex(particle) end return nil end ) end else local casterPos = GetGroundPosition( unit:GetAbsOrigin(), nil ) local targetPos = groundPos local particle = ParticleManager:CreateParticle("particles/econ/events/darkmoon_2017/darkmoon_calldown_marker_arrows.vpcf", PATTACH_CUSTOMORIGIN, nil) if particle ~= nil then ParticleManager:SetParticleControl(particle, 0, casterPos) ParticleManager:SetParticleControl(particle, 1, targetPos) ParticleManager:SetParticleControl( particle, 2, Vector(duration, 0, 0) ) self:startZoneTimer( id, duration, function() if particle ~= nil then ParticleManager:DestroyParticle(particle, false) ParticleManager:ReleaseParticleIndex(particle) end return nil end ) end end end local function createDirectionArrowNeutral(____, caster, targetPosition, warningDuration) local duration = warningDuration ~= nil and warningDuration or warningTimeNeutral local casterPos = GetGroundPosition( caster:GetAbsOrigin(), nil ) local groundTarget = GetGroundPosition(targetPosition, nil) local particle = ParticleManager:CreateParticle("particles/econ/events/darkmoon_2017/darkmoon_calldown_marker_arrows.vpcf", PATTACH_CUSTOMORIGIN, nil) if particle ~= nil then ParticleManager:SetParticleControl(particle, 0, casterPos) ParticleManager:SetParticleControl(particle, 1, groundTarget) ParticleManager:SetParticleControl( particle, 2, Vector(duration, 0, 0) ) self:startZoneTimer( id, duration, function() if particle ~= nil then ParticleManager:DestroyParticle(particle, false) ParticleManager:ReleaseParticleIndex(particle) end return nil end ) end end local pointFlagNeutral = 16 local unitTargetFlagNeutral = 32 local noTargetFlagNeutral = 4 if bit.band(behavior, unitTargetFlagNeutral) ~= 0 then local unitPos = unit:GetAbsOrigin() local targetPos = target:GetAbsOrigin() local dx = unitPos.x - targetPos.x local dy = unitPos.y - targetPos.y local distance = math.sqrt(dx * dx + dy * dy) if distance <= castRange and target:IsAlive() then if not suppressWarningParticles then createDirectionArrowNeutral(nil, unit, targetPos, warningTimeNeutral) end if not suppressWarningParticles then local overheadParticle = ParticleManager:CreateParticle("particles/econ/items/faceless_void/faceless_void_arcana/faceless_void_arcana_deny_v2_symbol_question.vpcf", PATTACH_OVERHEAD_FOLLOW, target) if overheadParticle ~= nil then self:startZoneTimer( id, warningTimeNeutral + 0.5, function() if overheadParticle ~= nil then ParticleManager:DestroyParticle(overheadParticle, false) ParticleManager:ReleaseParticleIndex(overheadParticle) end return nil end ) end end local direction = Vector(targetPos.x - unitPos.x, targetPos.y - unitPos.y, 0) local length = math.sqrt(direction.x * direction.x + direction.y * direction.y) if length > 0 then local normalizedDir = Vector(direction.x / length, direction.y / length, 0) unit:SetForwardVector(normalizedDir) end self:startZoneTimer( id, warningTimeNeutral, function() if not unit or not unit:IsAlive() or not target or not target:IsAlive() then return nil end ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_CAST_TARGET, TargetIndex = target:entindex(), AbilityIndex = ability:GetEntityIndex(), Queue = false }) return nil end ) neutralLastAbilityUseTime = now return true end elseif bit.band(behavior, pointFlagNeutral) ~= 0 then local unitPos = unit:GetAbsOrigin() local targetPos = target:GetAbsOrigin() local dx = unitPos.x - targetPos.x local dy = unitPos.y - targetPos.y local distance = math.sqrt(dx * dx + dy * dy) if distance <= castRange and target:IsAlive() then if abilityName ~= "frog_magi_wave" and not suppressWarningParticles then local ____table_GetAOERadius_15 if ability.GetAOERadius then ____table_GetAOERadius_15 = ability:GetAOERadius() else ____table_GetAOERadius_15 = 0 end local aoeRadius = ____table_GetAOERadius_15 local ____temp_16 if aoeRadius > 0 then ____temp_16 = aoeRadius else ____temp_16 = 200 end createPreCastParticle(nil, targetPos, ____temp_16, warningTimeNeutral) end local direction = Vector(targetPos.x - unitPos.x, targetPos.y - unitPos.y, 0) local length = math.sqrt(direction.x * direction.x + direction.y * direction.y) if length > 0 then local normalizedDir = Vector(direction.x / length, direction.y / length, 0) unit:SetForwardVector(normalizedDir) end self:startZoneTimer( id, warningTimeNeutral, function() if not unit or not unit:IsAlive() or not target or not target:IsAlive() then return nil end ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_CAST_POSITION, Position = targetPos, AbilityIndex = ability:GetEntityIndex(), Queue = false }) return nil end ) neutralLastAbilityUseTime = now return true end elseif bit.band(behavior, noTargetFlagNeutral) ~= 0 then local unitPos = unit:GetAbsOrigin() local targetPos = target:GetAbsOrigin() local dx = unitPos.x - targetPos.x local dy = unitPos.y - targetPos.y local distance = math.sqrt(dx * dx + dy * dy) local desiredDistance = 100 if distance > desiredDistance then ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_MOVE, TargetIndex = target:entindex(), Queue = false }) self:startZoneTimer( id, 0.3, function() if not unit or not unit:IsAlive() or not target or not target:IsAlive() then return nil end local newUnitPos = unit:GetAbsOrigin() local newTargetPos = target:GetAbsOrigin() local ndx = newUnitPos.x - newTargetPos.x local ndy = newUnitPos.y - newTargetPos.y local newDistance = math.sqrt(ndx * ndx + ndy * ndy) if newDistance <= desiredDistance + 25 then ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_CAST_NO_TARGET, AbilityIndex = ability:GetEntityIndex(), Queue = false }) neutralLastAbilityUseTime = now return nil end return 0.2 end ) return true end local lockedTargetPos = target:GetAbsOrigin() local ____table_GetAOERadius_17 if ability.GetAOERadius then ____table_GetAOERadius_17 = ability:GetAOERadius() else ____table_GetAOERadius_17 = 0 end local aoeRadius = ____table_GetAOERadius_17 if aoeRadius <= 0 and ability.GetSpecialValueFor then local specialRadius = ability:GetSpecialValueFor("radius") if specialRadius and specialRadius > 0 then aoeRadius = specialRadius end end if not suppressWarningParticles then local ____temp_18 if aoeRadius > 0 then ____temp_18 = aoeRadius else ____temp_18 = 200 end local followRadius = ____temp_18 local followParticle = ParticleManager:CreateParticle("particles/econ/events/darkmoon_2017/darkmoon_generic_aoe.vpcf", PATTACH_CUSTOMORIGIN, nil) if followParticle ~= nil then ParticleManager:SetParticleControlEnt( followParticle, 0, unit, PATTACH_ABSORIGIN_FOLLOW, nil, unit:GetAbsOrigin(), true ) ParticleManager:SetParticleControl( followParticle, 1, Vector(followRadius, 0, 0) ) ParticleManager:SetParticleControl( followParticle, 2, Vector(6, 0, 1) ) ParticleManager:SetParticleControl( followParticle, 3, Vector(200, 0, 0) ) ParticleManager:SetParticleControl( followParticle, 4, unit:GetAbsOrigin() ) self:startZoneTimer( id, warningTimeNeutral, function() if followParticle ~= nil then ParticleManager:DestroyParticle(followParticle, false) ParticleManager:ReleaseParticleIndex(followParticle) end return nil end ) end end do local direction = Vector(lockedTargetPos.x - unitPos.x, lockedTargetPos.y - unitPos.y, 0) local length = math.sqrt(direction.x * direction.x + direction.y * direction.y) if length > 0 then local normalizedDir = Vector(direction.x / length, direction.y / length, 0) unit:SetForwardVector(normalizedDir) end end if not suppressWarningParticles then createDirectionArrowNeutral(nil, unit, lockedTargetPos, warningTimeNeutral) end self:startZoneTimer( id, warningTimeNeutral, function() if not unit or not unit:IsAlive() then return nil end local currentPos = unit:GetAbsOrigin() local direction = Vector(lockedTargetPos.x - currentPos.x, lockedTargetPos.y - currentPos.y, 0) local length = math.sqrt(direction.x * direction.x + direction.y * direction.y) if length > 0 then local normalizedDir = Vector(direction.x / length, direction.y / length, 0) unit:SetForwardVector(normalizedDir) end ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_CAST_NO_TARGET, AbilityIndex = ability:GetEntityIndex(), Queue = false }) return nil end ) neutralLastAbilityUseTime = now return true end end ::__continue340:: i = i + 1 end end return false end checkAggression = function() if not unit or not unit:IsAlive() then return end local currentHealth = unit:GetHealth() local maxHealth = unit:GetMaxHealth() local currentPos = unit:GetAbsOrigin() local now = GameRules:GetGameTime() if config.moveInGroups then local group = self:getGroupForUnit(unit) if group and #group > 0 then local leader = self:getGroupLeader(group) if leader and leader ~= unit and leader:IsAlive() then local leaderPos = leader:GetAbsOrigin() local followRadius = config.moveInGroups.stayRadius or 250 local dxF = currentPos.x - leaderPos.x local dyF = currentPos.y - leaderPos.y local distToLeader = math.sqrt(dxF * dxF + dyF * dyF) if distToLeader > followRadius * 0.7 and not unit:IsAttacking() and not unit:IsChanneling() then ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = leaderPos, Queue = false }) return 0.2 end end end end if not self:isWithinZoneBounds(config, currentPos) then local nearbyEnemiesRaw = FindUnitsInRadius( unit:GetTeamNumber(), currentPos, nil, 200, DOTA_UNIT_TARGET_TEAM_ENEMY, bit.bor(DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_BASIC), DOTA_UNIT_TARGET_FLAG_NONE, FIND_CLOSEST, false ) local nearbyEnemies = __TS__ArrayFilter( nearbyEnemiesRaw, function(____, e) return self:isWithinChaseRange( config, e:GetAbsOrigin() ) end ) if #nearbyEnemies == 0 then local deepPos = self:getDeepPositionInZone(config) ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = deepPos, Queue = false }) self:startZoneTimer( id, 1, function() if not unit or not unit:IsAlive() then return nil end local checkPos = unit:GetAbsOrigin() if not self:isWithinZoneBounds(config, checkPos) then ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = self:getDeepPositionInZone(config), Queue = false }) end return nil end ) return 0.1 end end if currentHealth < maxHealth and not isAggressive then isAggressive = true unit:SetAttackCapability(DOTA_UNIT_CAP_MELEE_ATTACK) local nearbyUnitsRaw = FindUnitsInRadius( unit:GetTeamNumber(), unit:GetAbsOrigin(), nil, 500, DOTA_UNIT_TARGET_TEAM_ENEMY, DOTA_UNIT_TARGET_HERO + DOTA_UNIT_TARGET_BASIC, DOTA_UNIT_TARGET_FLAG_NONE, FIND_CLOSEST, false ) local nearbyUnits = __TS__ArrayFilter( nearbyUnitsRaw, function(____, e) return self:isWithinChaseRange( config, e:GetAbsOrigin() ) end ) if #nearbyUnits > 0 then ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET, TargetIndex = nearbyUnits[1]:entindex(), Queue = false }) end self:startZoneTimer( id, 5, function() if not unit or not unit:IsAlive() then return nil end isAggressive = false unit:SetAttackCapability(DOTA_UNIT_CAP_NO_ATTACK) local returnPos = config.moveInGroups and self:getGroupForUnit(unit) and self:getGroupCenter(self:getGroupForUnit(unit)) or spawnPosition ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = returnPos, Queue = false }) return nil end ) end if isAggressive then local detectionRadius = 2000 local nearbyEnemiesRaw = FindUnitsInRadius( unit:GetTeamNumber(), unit:GetAbsOrigin(), nil, detectionRadius, DOTA_UNIT_TARGET_TEAM_ENEMY, bit.bor(DOTA_UNIT_TARGET_HERO, DOTA_UNIT_TARGET_BASIC), DOTA_UNIT_TARGET_FLAG_NONE, FIND_CLOSEST, false ) local nearbyEnemies = __TS__ArrayFilter( nearbyEnemiesRaw, function(____, e) return self:isWithinChaseRange( config, e:GetAbsOrigin() ) end ) if #nearbyEnemies > 0 then local closestEnemy = nearbyEnemies[1] local unitPos = unit:GetAbsOrigin() local enemyPos = closestEnemy:GetAbsOrigin() local dx = unitPos.x - enemyPos.x local dy = unitPos.y - enemyPos.y local distanceToEnemy = math.sqrt(dx * dx + dy * dy) if distanceToEnemy <= detectionRadius and closestEnemy:IsAlive() then if distanceToEnemy > unit:GetBaseAttackRange() * 1.2 then ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_MOVE, Position = enemyPos, Queue = false }) else ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET, TargetIndex = closestEnemy:entindex(), Queue = false }) end if distanceToEnemy <= 800 then local usedAbility = tryUseAbilitiesNeutral(nil) if usedAbility then return 0.1 end end end end end local currentPosCheck = unit:GetAbsOrigin() if not self:isWithinZoneBounds(config, currentPosCheck) and unit:GetAttackCapability() == DOTA_UNIT_CAP_NO_ATTACK then local newPos = self:clampToZoneBounds(config, currentPosCheck) ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = newPos, Queue = false }) end return 0.1 end unit:SetContextThink("AggressionCheck", checkAggression, 0) break end end until true local function getRandomWalkPosition() local factor = config.patrolRadiusFactor ~= nil and math.max( 0.1, math.min(1, config.patrolRadiusFactor) ) or 1 local basePos = config.position if config.moveInGroups then local group = self:getGroupForUnit(unit) if group and #group > 0 then basePos = self:getGroupCenter(group) end end local validPos if config.shape == "square" then local halfWidth = (config.width ~= nil and config.width / 2 or config.radius) * factor local halfHeight = (config.height ~= nil and config.height / 2 or config.radius) * factor local offsetX = RandomFloat(-halfWidth, halfWidth) local offsetY = RandomFloat(-halfHeight, halfHeight) validPos = GetGroundPosition( Vector(basePos.x + offsetX, basePos.y + offsetY, basePos.z), nil ) else local angle = RandomFloat(0, 2 * math.pi) local distance = RandomFloat(0, config.radius * factor) validPos = GetGroundPosition( Vector( basePos.x + distance * math.cos(angle), basePos.y + distance * math.sin(angle), basePos.z ), nil ) end if not self:isWithinZoneBounds(config, validPos) then return self:clampToZoneBounds(config, validPos) end return validPos end local moveRandomly moveRandomly = function() if not self.spawnConfigs:has(id) then return end if not unit or not unit:IsAlive() then return end unit:SetForceAttackTarget(nil) if unit:GetAttackCapability() == DOTA_UNIT_CAP_MELEE_ATTACK or unit:GetAttackCapability() == DOTA_UNIT_CAP_RANGED_ATTACK then local newPos = getRandomWalkPosition(nil) ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_ATTACK_MOVE, Position = newPos, Queue = false }) end if unit:GetAttackCapability() == DOTA_UNIT_CAP_NO_ATTACK then local newPos = getRandomWalkPosition(nil) ExecuteOrderFromTable({ UnitIndex = unit:entindex(), OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION, Position = newPos, Queue = false }) end Timers:CreateTimer( RandomFloat(3, 7), function() if not self.spawnConfigs:has(id) then return nil end if unit and unit:IsAlive() then moveRandomly(nil) end return nil end ) end unit:SetMoveCapability(DOTA_UNIT_CAP_MOVE_GROUND) Timers:CreateTimer( 1, function() if not self.spawnConfigs:has(id) then return nil end moveRandomly(nil) return nil end ) aliveUnits[#aliveUnits + 1] = unit self.spawnedUnits:set(id, aliveUnits) self.spawnTimers:set( id, GameRules:GetGameTime() ) end end end function SpawnManager.prototype.RemoveSpawnZone(self, id) self:cancelZoneTimers(id) local units = self.spawnedUnits:get(id) or ({}) for ____, unit in ipairs(units) do if unit and IsValidEntity(unit) then do pcall(function() unit:SetContextThink("AggressionCheck", nil, 0) unit:RemoveSelf() end) end end end local prefix = id .. "_grp_" for ____, groupId in ipairs(__TS__ArrayFrom(self.unitGroups:keys())) do if __TS__StringStartsWith(groupId, prefix) then self.unitGroups:delete(groupId) end end self.spawnConfigs:delete(id) self.spawnTimers:delete(id) self.spawnedUnits:delete(id) self.zoneRespawnScheduled:delete(id) if self.debugZonesVisible then DebugDrawClear() self.spawnConfigs:forEach(function(____, config, zoneId) self:DrawSpawnZoneDebug(zoneId, config) end) end end function SpawnManager.prototype.RemoveSpawnZonesByType(self, ____type) local zoneIds = __TS__ArrayFrom(self.spawnConfigs:keys()) __TS__ArrayForEach( zoneIds, function(____, zoneId) if __TS__StringStartsWith(zoneId, ("zone_" .. ____type) .. "_") then self:RemoveSpawnZone(zoneId) end end ) end function SpawnManager.prototype.RemoveAllSpawnZones(self) local zoneIds = __TS__ArrayFrom(self.spawnConfigs:keys()) __TS__ArrayForEach( zoneIds, function(____, zoneId) self:RemoveSpawnZone(zoneId) end ) end function SpawnManager.prototype.GetSpawnedUnits(self, id) return self.spawnedUnits:get(id) or ({}) end function SpawnManager.prototype.GetZoneIdsByPrefixes(self, prefixes) local result = {} self.spawnConfigs:forEach(function(____, _, zoneId) for ____, prefix in ipairs(prefixes) do if __TS__StringStartsWith(zoneId, prefix) then result[#result + 1] = zoneId break end end end) return result end function SpawnManager.prototype.GetZoneCentersByPrefixes(self, prefixes) local centers = {} self.spawnConfigs:forEach(function(____, config, zoneId) for ____, prefix in ipairs(prefixes) do if __TS__StringStartsWith(zoneId, prefix) and config.position then centers[#centers + 1] = config.position break end end end) return centers end function SpawnManager.prototype.getSpawnUnitConfigName(self, unit) do local function ____catch() return true, nil end local ____try, ____hasReturned, ____returnValue = pcall(function() local stored = unit.__spawnManagerUnitName if stored and #stored > 0 then return true, stored end local name = unit:GetUnitName() return true, name and #name > 0 and name or nil end) if not ____try then ____hasReturned, ____returnValue = ____catch() end if ____hasReturned then return ____returnValue end end end function SpawnManager.prototype.getValidSpawnedUnits(self, zoneId) local units = self.spawnedUnits:get(zoneId) or ({}) local validUnits = __TS__ArrayFilter( units, function(____, unit) if not unit or not IsValidEntity(unit) then return false end do local function ____catch() return true, false end local ____try, ____hasReturned, ____returnValue = pcall(function() return true, unit:IsAlive() end) if not ____try then ____hasReturned, ____returnValue = ____catch() end if ____hasReturned then return ____returnValue end end end ) self.spawnedUnits:set(zoneId, validUnits) return validUnits end function SpawnManager.prototype.countSpawnedUnitsByConfigName(self, zoneId) local counts = __TS__New(Map) for ____, unit in ipairs(self:getValidSpawnedUnits(zoneId)) do do local name = self:getSpawnUnitConfigName(unit) if not name then goto __continue478 end counts:set( name, (counts:get(name) or 0) + 1 ) end ::__continue478:: end return counts end function SpawnManager.prototype.getSpawnDelayForUnit(self, config, unitInfo) return __TS__StringIncludes(unitInfo.unitName, "zombie") and config.interval * 2 or config.interval end function SpawnManager.prototype.replenishZoneMissingUnits(self, zoneId) local config = self.spawnConfigs:get(zoneId) if not config then return end if not self:isSpawnAllowed() then self:startZoneTimer( zoneId, 0.5, function() self:replenishZoneMissingUnits(zoneId) return nil end ) return end local currentUnitCounts = self:countSpawnedUnitsByConfigName(zoneId) local toSpawn = {} for ____, unitInfo in ipairs(config.units) do local currentCount = currentUnitCounts:get(unitInfo.unitName) or 0 local deficit = math.max(0, unitInfo.count - currentCount) do local i = 0 while i < deficit do toSpawn[#toSpawn + 1] = unitInfo i = i + 1 end end end if #toSpawn <= 0 then return end do local i = 0 while i < #toSpawn do local unitInfo = toSpawn[i + 1] local spawnDelay = self:getSpawnDelayForUnit(config, unitInfo) + i * 0.15 self:startZoneTimer( zoneId, spawnDelay, function() self:SpawnUnit(zoneId, config, unitInfo) return nil end ) i = i + 1 end end end function SpawnManager.prototype.QueueRespawn(self, zoneId) if not self.spawnConfigs:get(zoneId) then return end if self.zoneRespawnScheduled:get(zoneId) then return end self.zoneRespawnScheduled:set(zoneId, true) self:startZoneTimer( zoneId, 0.5, function() self.zoneRespawnScheduled:delete(zoneId) self:replenishZoneMissingUnits(zoneId) return nil end ) end function SpawnManager.prototype.log(self, message) end SpawnManager.CHASE_LEASH_MARGIN = 350 --- Маркеры зон спавна / визуалы событий function ____exports.precacheSpawnManagerEffectParticles(self, context) PrecacheResource("particle", "particles/econ/events/darkmoon_2017/darkmoon_generic_aoe.vpcf", context) PrecacheResource("particle", "particles/econ/events/darkmoon_2017/darkmoon_calldown_marker_arrows.vpcf", context) PrecacheResource("particle", "particles/econ/items/faceless_void/faceless_void_arcana/faceless_void_arcana_deny_v2_symbol_question.vpcf", context) end return ____exports