2902 lines
159 KiB
Lua
2902 lines
159 KiB
Lua
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
|