camps.pull_window(name)#camps
Neutral camp data the stacking packs share: valve's stack window for each camp, the trained stack data (camp positions, approach and pull points, difficulties) and the stacking routines that jungle, s
"requires": { "camps": "^1.0" }local camps = require("lib/camps")Players install nothing: the script brings the library, any version inside the range. Edit scripts/lib/camps/ in a dev session to try a change before you publish.
lib/camps
Neutral camp data the stacking packs share: valve's stack window for each camp,
the trained stack data (camp positions, approach and pull points, difficulties) and the
stacking routines that jungle, summons and lycan carried word for word. Moved out of
uTEA/jungle/stack_data, uTEA/heroes/naga/stack_data and the three packs (#132) without
changing what they compute; jungle, summons, lycan, naga and chen use it.
local Camps = require("lib/camps")
local F = {}
Camps.install_stacking(F) -- the shared routines, defined on F
F.STACK_AGGRO_DEFAULT = Camps.STACK_AGGRO_DEFAULT
local aggro = Camps.aggro_second("neutralcamp_good_2") -- 53: valve hits it at 52
local naga = Camps.stack_state("naga") -- the session table of data/naga.lua
local pts = naga.stackPoints.approaches["14"] -- { x, y, z }
data
| file | trained | read by |
|---|---|---|
data/jungle.lua |
empty | jungle, summons, lycan |
data/naga.lua |
difficulties 28 camps, positions 26, approach points 29, pull points 28, pull directions 5 | naga, chen (stack points), lycan (fills the camps jungle's data lacks) |
A data table has version, campPositions, campDifficulties, stackPoints.approaches,
stackPoints.pulls and pullDirections, keyed by camp slot id as a string. naga's file keeps
camp positions as python repr strings; copy_state parses them.
stack_state(name) returns one table per name for the whole session, the module
package.loaded["lib/camps/data/<name>"]. It sits outside the utea purge, so a utea reload keeps
the points a pack set during the session. save_stack_state writes a data file through io,
which the dll does not open (script.cpp:1393): in the client it returns false and edits last
until the client closes. Offline (lua.exe, tools) it writes scripts/lib/camps/data/<name>.lua.
api
| name | what |
|---|---|
STACK_AGGRO_DEFAULT, STACK_ATTACK_SECOND_OFFSET |
53, -1 |
PULL_TIMINGS |
valve's { start, stop, dir } by spawn box name (data/pull_timings.lua, generated by tools/farmplan/gen_pull_timings.py from creep_pull_timings.txt) |
pull_window(name) |
that row for one box, or nil |
aggro_second(name) |
second of the minute the box starts chasing its attacker: valve's start + 1, so a pack attacking at aggro + STACK_ATTACK_SECOND_OFFSET hits on valve's second; 53 for a name valve does not list |
stack_state(name) |
the session table of data/<name>.lua, missing fields filled; an empty one when there is no file |
save_stack_state(name, state?) |
writes state (default: the session table); false without io. A table that is not the session one is written but does not replace it |
encode(state) |
the lua source of a data file, keys sorted |
copy_state(state) |
a new table: string keys, {x, y, z} points, python repr positions parsed, junk dropped |
merge_missing(state, defaults) |
fills the camps, points and directions state lacks; both copy_state shaped |
install_stacking(F) |
defines the routines below on F, returns F |
The timings are keyed by the box name (strCampName), so a patch that adds or reorders camps
does not shift them. utea/core/farmplan/pull_timings_generated.lua is a one-line redirect
here for autopilot/stack.lua and farmplan/route.lua.
install_stacking
Defines 51 functions on F (M.NAMES in stacking.lua). Each reaches the rest of the pack
through F, so a pack replaces one by defining it after the install.
| group | functions |
|---|---|
| units | get_time is_near_fountain get_true_damage get_aps get_armor_mult get_health get_unit_lifetime get_unit_attack_point |
| timing and travel | get_stack_travel_cache_key get_stack_candidate_travel_time get_shortest_box_exit get_best_pull_direction get_stack_pull_target get_stack_base_approach get_stack_stage_pos get_stack_attack_second get_stack_timing_safety get_stack_timing_difficulty get_seconds_until_next_minute get_stack_forecast_creep_exit_time get_stack_forecast_pull_survival_time get_stack_forecast_attack_deadline is_ancient_slot is_press_now_stack_candidate get_stack_security_priority get_stack_attack_prep_from_pos |
| camp slots | infer_base_type build_slot_id fill_slots_once spawn_check_points build_power_snapshot evaluate_visible_pack mark_slot_empty reset_camp_cycle |
| trained points | get_jungle_state vec_to_point point_to_vec load_camp_positions apply_stack_approaches load_pull_directions find_stack_edit_slot set_stack_approach_point set_stack_pull_point |
| assignment | slot_is_safe get_camp_difficulty is_early_level find_nearest_alive_slot count_existing_assignments assign_early_gather get_exit_pos update_stack_stats |
The pack provides, on F:
- constants:
ANCIENT_CAMP_INDICES NEUTRAL_CREEP_AVERAGE_MS STACK_AGGRO_DEFAULT STACK_APPROACH_POSITIONS STACK_ATTACK_BUFFER STACK_ATTACK_POINT_DEFAULT STACK_ATTACK_SECOND_OFFSET STACK_CREEP_EXIT_BUFFER STACK_EASY_SAFETY_SECONDS STACK_MEDIUM_SAFETY_SECONDS STACK_PULL_ARRIVAL_RADIUS STACK_PULL_DISTANCE STACK_PULL_HOLD_TIME STACK_PULL_TARGETS STACK_STAGE_OFFSET - functions:
build_candidate_list can_run_stack_grid_path dist get_dota_clock get_grid_path_distance_once get_own_fountain_pos get_seconds_until_aggro get_stack_attack_target_distance handle_camp_clear is_camp_cleared is_our_side_slot read_jungle_state_from_module safe_call save_stack_points score_pull_direction
Every install makes new closures; nothing is shared between two packs' F.
needs
GameRules, NPC, Entity, Modifier, Player, Players from the compat layer
(compat/, loaded by utea.lua:84), the native Vector and gamerules.neutral_spawn_boxes().
A caller outside utea loads the compat shims first.
test
tools\lua\lua.exe scripts\lib\camps\selftest.lua from the repo root: valve's timings, the shared tables, the
copy of naga's data, the encoder round trip, the save path with a stubbed io, and that an
install defines exactly the 51 routines bound to its own F.
The code of 1.0.0 as the client gets it. The bundle's bytecode is built from these files.
init.lua243 lines
---@diagnostic disable: undefined-global
-- lib/camps: neutral camp data the stacking packs share (jungle, summons, lycan, naga, chen,
-- natureprophet): valve's stack window per camp, the stack data store, and the stacking
-- routines jungle, summons and lycan carried word for word (#132). api and deps in README.md.
--
-- Owns: package.loaded["lib/camps/data/<name>"], the session copy of each data file
-- Must not: issue orders or require uTEA/*.
local Camps = {}
-- valve's stack window by spawn box name, { start, stop, dir } (data/pull_timings.lua, from
-- creep_pull_timings.txt). the one source of camp timings: autopilot/stack and
-- farmplan/route read the same table
local okP, PULL = pcall(require, "lib/camps/data/pull_timings")
if not (okP and type(PULL) == "table") then PULL = {} end
Camps.PULL_TIMINGS = PULL
Camps.STACK_AGGRO_DEFAULT = 53
Camps.STACK_ATTACK_SECOND_OFFSET = -1
function Camps.pull_window(name)
return PULL[name]
end
-- second of the minute a camp starts chasing its attacker. packs attack at
-- aggro + STACK_ATTACK_SECOND_OFFSET, which is valve's start
function Camps.aggro_second(name)
local w = PULL[name]
if w and w.start then return w.start - Camps.STACK_ATTACK_SECOND_OFFSET end
return Camps.STACK_AGGRO_DEFAULT
end
-- ======== stack data store ========
local DATA = "lib/camps/data/"
local written = {} -- name -> path the last save wrote
local function fill(state)
state = type(state) == "table" and state or {}
state.version = state.version or 1
state.campPositions = state.campPositions or {}
state.campDifficulties = state.campDifficulties or {}
state.stackPoints = state.stackPoints or {}
state.stackPoints.approaches = state.stackPoints.approaches or {}
state.stackPoints.pulls = state.stackPoints.pulls or {}
state.pullDirections = state.pullDirections or {}
return state
end
-- the session table of data/<name>.lua, its missing fields filled in place. every caller of a
-- name gets the same table, and a utea reload does not reset it
function Camps.stack_state(name)
local key = DATA .. name
local ok, data = pcall(require, key)
if not ok or type(data) ~= "table" then
data = {}
package.loaded[key] = data
end
return fill(data)
end
local function quote(value)
local text = tostring(value)
text = text:gsub("\\", "\\\\"):gsub("\"", "\\\""):gsub("\n", "\\n"):gsub("\r", "\\r")
return "\"" .. text .. "\""
end
local function table_key(key)
if type(key) == "number" then return "[" .. tostring(key) .. "]" end
return "[" .. quote(key) .. "]"
end
local function scalar(value)
local value_type = type(value)
if value_type == "number" or value_type == "boolean" then return tostring(value) end
return quote(value)
end
local function serialize(value, indent)
indent = indent or ""
if type(value) ~= "table" then return scalar(value) end
local keys = {}
for key in pairs(value) do keys[#keys + 1] = key end
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
if #keys == 0 then return "{}" end
local next_indent = indent .. " "
local lines = { "{" }
for _, key in ipairs(keys) do
lines[#lines + 1] = next_indent .. table_key(key) .. " = " .. serialize(value[key], next_indent) .. ","
end
lines[#lines + 1] = indent .. "}"
return table.concat(lines, "\n")
end
-- the lua source of a data file, keys sorted
function Camps.encode(state)
return "-- Persistent stack data, written by lib/camps.\n"
.. "return " .. serialize(state, "") .. "\n"
end
local function write_file(path, content)
if type(io) ~= "table" or type(io.open) ~= "function" then return false end
local file = io.open(path, "w")
if not file then return false end
file:write(content)
file:close()
return true
end
-- writes state (default: the session table) to data/<name>.lua. needs io, which the evolve
-- dll does not open (script.cpp:1393), so in the client this returns false and the data lives
-- for the session. a table that is not the session one is written but does not replace it
function Camps.save_stack_state(name, state)
state = fill(state or Camps.stack_state(name))
local content = Camps.encode(state)
local paths = { "scripts/" .. DATA .. name .. ".lua", DATA .. name .. ".lua" }
if written[name] then table.insert(paths, 1, written[name]) end
for _, path in ipairs(paths) do
if write_file(path, content) then
written[name] = path
return true
end
end
return false
end
-- ======== a normalized copy ========
local function point(p)
if type(p) ~= "table" then return nil end
local x = p.x or p[1]
local y = p.y or p[2]
local z = p.z or p[3] or 0
if type(x) ~= "number" or type(y) ~= "number" then return nil end
return { x = x, y = y, z = z }
end
local function point_map(points)
local out = {}
if type(points) ~= "table" then return out end
for key, p in pairs(points) do
local q = point(p)
if q then out[tostring(key)] = q end
end
return out
end
local function pull_direction_map(points)
local out = point_map(points)
if type(points) ~= "table" then return out end
for key, p in pairs(points) do
if type(p) == "table" and out[tostring(key)] then
out[tostring(key)].edge = p.edge
out[tostring(key)].v = p.v
end
end
return out
end
-- naga's data keeps camp positions as python repr strings: "[{'x': 1, 'y': 2, 'z': 3}, ...]"
local function legacy_camp_positions(text)
local out = {}
if type(text) ~= "string" then return out end
for x, y, z in text:gmatch("{%s*'x':%s*([%-%.%d]+),%s*'y':%s*([%-%.%d]+),%s*'z':%s*([%-%.%d]+)%s*}") do
out[#out + 1] = { x = tonumber(x), y = tonumber(y), z = tonumber(z) or 0 }
end
return out
end
local function point_list(points)
if type(points) == "string" then return legacy_camp_positions(points) end
local out = {}
if type(points) ~= "table" then return out end
for _, p in ipairs(points) do
local q = point(p)
if q then out[#out + 1] = q end
end
return out
end
local function camp_positions(positions)
local out = {}
if type(positions) ~= "table" then return out end
for key, points in pairs(positions) do
local list = point_list(points)
if #list > 0 then out[tostring(key)] = list end
end
return out
end
local function string_map(values)
local out = {}
if type(values) ~= "table" then return out end
for key, value in pairs(values) do
if type(value) == "string" then out[tostring(key)] = value end
end
return out
end
-- a new table: string keys, {x,y,z} points, python repr positions parsed, junk dropped
function Camps.copy_state(state)
state = type(state) == "table" and state or {}
local stack_points = type(state.stackPoints) == "table" and state.stackPoints or {}
return {
version = state.version or 1,
campPositions = camp_positions(state.campPositions),
campDifficulties = string_map(state.campDifficulties),
stackPoints = {
approaches = point_map(stack_points.approaches),
pulls = point_map(stack_points.pulls),
},
pullDirections = pull_direction_map(state.pullDirections),
}
end
local function merge_missing_entries(target, source)
if type(target) ~= "table" or type(source) ~= "table" then return end
for key, value in pairs(source) do
if target[key] == nil then target[key] = value end
end
end
-- fills the camps, points and directions state lacks from defaults; both are copy_state shaped
function Camps.merge_missing(state, defaults)
if type(state) ~= "table" or type(defaults) ~= "table" then return state end
merge_missing_entries(state.campPositions, defaults.campPositions)
merge_missing_entries(state.campDifficulties, defaults.campDifficulties)
merge_missing_entries(state.stackPoints.approaches, defaults.stackPoints.approaches)
merge_missing_entries(state.stackPoints.pulls, defaults.stackPoints.pulls)
merge_missing_entries(state.pullDirections, defaults.pullDirections)
return state
end
-- ======== stacking routines ========
-- defines the shared stacking functions on F (list: README.md); returns F
local stacking = require("lib/camps/stacking")
function Camps.install_stacking(F)
return stacking.install(F, Camps)
end
return Camps
pack.json2 lines
{ "kind": "lib", "version": "1.0.0" }
stacking.lua688 lines
---@diagnostic disable: undefined-global
-- lib/camps/stacking: the camp-stacking routines that jungle, summons and lycan carried word for
-- word (#132). install(F, Camps) defines them on a pack's F; they reach the rest of the pack through F,
-- so a pack overrides one by defining it after install. list and expected F.* in README.md.
local M = {}
function M.install(F, Camps)
local function camp_boxes()
local ok, rows = pcall(function() return gamerules.neutral_spawn_boxes() end)
if not ok or type(rows) ~= "table" or #rows == 0 then return nil end
local out = {}
for _, r in ipairs(rows) do
local inner = { m_vMinBounds = r.min, m_vMaxBounds = r.max }
local box = { strCampName = r.name,
neutralSpawnBoxes = { GetField = function(k) return inner[k] end } }
out[#out + 1] = { GetField = function(k) return box[k] end }
end
return out
end
function F.get_time() return GameRules.GetGameTime() end
function F.is_near_fountain(pos, hero)
return F.dist(pos, F.get_own_fountain_pos(hero)) < 3000
end
function F.get_true_damage(unit) return F.safe_call(NPC.GetTrueDamage, 25, unit) end
function F.get_aps(unit)
local v = F.safe_call(NPC.GetAttacksPerSecond, 0.7, unit)
return (v and v > 0) and v or 0.7
end
function F.get_armor_mult(unit)
local v = F.safe_call(NPC.GetArmorDamageMultiplier, 1.0, unit)
return (v and v > 0) and v or 1.0
end
function F.get_health(unit) return F.safe_call(Entity.GetHealth, 0, unit) end
function F.get_unit_lifetime(unit)
local mod = NPC.GetModifier(unit, "modifier_illusion")
if mod then
local die = F.safe_call(Modifier.GetDieTime, 0, mod)
if die and die > 0 then return math.max(die - F.get_time(), 0) end
end
local mod2 = NPC.GetModifier(unit, "modifier_kill")
if mod2 then
local die = F.safe_call(Modifier.GetDieTime, 0, mod2)
if die and die > 0 then return math.max(die - F.get_time(), 0) end
end
return 999
end
function F.get_unit_attack_point(unit)
local attack_point = F.STACK_ATTACK_POINT_DEFAULT
if not unit then return attack_point end
local kv = NPC.GetKeyValues(unit)
if kv and type(kv) == "table" then
local ap = kv.AttackAnimationPoint
if type(ap) == "number" and ap > 0 then attack_point = ap end
end
return attack_point
end
function F.get_stack_travel_cache_key(from_pos, slot)
local fx = math.floor(from_pos:GetX() / 256)
local fy = math.floor(from_pos:GetY() / 256)
return fx .. "_" .. fy .. "_s" .. slot.id
end
function F.get_stack_candidate_travel_time(m, from_pos, approach, move_speed, slot, allow_estimate)
if not move_speed or move_speed <= 0 then return nil end
local ckey = F.get_stack_travel_cache_key(from_pos, slot)
if m._stackTravelCache and m._stackTravelCache[ckey] then
return m._stackTravelCache[ckey]
end
local grid_dist = F.can_run_stack_grid_path(m)
and F.get_grid_path_distance_once(from_pos, approach) or nil
if grid_dist then
local t = grid_dist / move_speed
if not m._stackTravelCache then m._stackTravelCache = {} end
m._stackTravelCache[ckey] = t
return t
end
if allow_estimate ~= false then
return F.dist(from_pos, approach) / move_speed
end
return nil
end
function F.get_shortest_box_exit(slot)
local cx = (slot.mins:GetX() + slot.maxs:GetX()) * 0.5
local cy = (slot.mins:GetY() + slot.maxs:GetY()) * 0.5
local half_w = (slot.maxs:GetX() - slot.mins:GetX()) * 0.5
local half_h = (slot.maxs:GetY() - slot.mins:GetY()) * 0.5
return math.min(half_w, half_h)
end
function F.get_best_pull_direction(slot, m)
if slot._pullDirCached then return slot._pullDirCached end
local best_dir, best_score = nil, -999999
for a = 0, 15 do
local rad = a * math.pi / 8
local dir = Vector(math.cos(rad), math.sin(rad), 0)
local score = F.score_pull_direction(slot, dir, m)
if score > best_score then best_score = score; best_dir = dir end
end
slot._pullDirCached = best_dir or Vector(1, 0, 0)
return slot._pullDirCached
end
function F.get_stack_pull_target(slot, m)
if slot._savedPullTarget then return slot._savedPullTarget end
local cx = (slot.mins:GetX() + slot.maxs:GetX()) * 0.5
local cy = (slot.mins:GetY() + slot.maxs:GetY()) * 0.5
local dir = F.get_best_pull_direction(slot, m)
return Vector(
cx + dir:GetX() * F.STACK_PULL_DISTANCE,
cy + dir:GetY() * F.STACK_PULL_DISTANCE,
slot.center:GetZ()
)
end
function F.get_stack_base_approach(slot, m)
if slot._savedApproach then return slot._savedApproach end
local cx = (slot.mins:GetX() + slot.maxs:GetX()) * 0.5
local cy = (slot.mins:GetY() + slot.maxs:GetY()) * 0.5
local pull_dir = F.get_best_pull_direction(slot, m)
return Vector(
cx + pull_dir:GetX() * F.STACK_PULL_DISTANCE * 0.15,
cy + pull_dir:GetY() * F.STACK_PULL_DISTANCE * 0.15,
slot.center:GetZ()
)
end
function F.get_stack_stage_pos(slot)
local cx = (slot.mins:GetX() + slot.maxs:GetX()) * 0.5
local cy = (slot.mins:GetY() + slot.maxs:GetY()) * 0.5
local half_w = (slot.maxs:GetX() - slot.mins:GetX()) * 0.5
local half_h = (slot.maxs:GetY() - slot.mins:GetY()) * 0.5
local exit_dist = math.min(half_w, half_h) + F.STACK_STAGE_OFFSET
return Vector(cx + exit_dist, cy, slot.center:GetZ())
end
function F.get_stack_attack_second(slot)
return ((slot.stackAggroTime or F.STACK_AGGRO_DEFAULT) + F.STACK_ATTACK_SECOND_OFFSET) % 60
end
function F.get_stack_timing_safety(slot)
return (60 - F.get_stack_attack_second(slot)) % 60
end
function F.get_stack_timing_difficulty(slot)
local safety = F.get_stack_timing_safety(slot)
if safety >= F.STACK_EASY_SAFETY_SECONDS then return 1 end
if safety >= F.STACK_MEDIUM_SAFETY_SECONDS then return 2 end
return 3
end
function F.get_seconds_until_next_minute()
local clock = F.get_dota_clock()
if clock < 0 then return 60 end
return 60 - (clock % 60)
end
function F.get_stack_forecast_creep_exit_time(slot, m)
local exit_dist = F.get_shortest_box_exit(slot) + F.STACK_CREEP_EXIT_BUFFER * F.NEUTRAL_CREEP_AVERAGE_MS
return exit_dist / F.NEUTRAL_CREEP_AVERAGE_MS
end
function F.get_stack_forecast_pull_survival_time(slot, m, move_speed)
local pull_target = F.get_stack_pull_target(slot, m)
local dist = math.max(F.dist(slot.center, pull_target) - F.STACK_PULL_ARRIVAL_RADIUS, 0)
local travel = dist / move_speed
return travel + F.STACK_PULL_HOLD_TIME
end
function F.get_stack_forecast_attack_deadline(slot, m)
return F.get_seconds_until_aggro(slot)
end
function F.is_ancient_slot(slot)
if slot.manualDifficulty == "Ancient" then return true end
if F.ANCIENT_CAMP_INDICES[slot.index] then return true end
return slot.baseType == "Ancient" or slot.predictedClass == "Ancient"
end
function F.is_press_now_stack_candidate(slot, hero)
if not F.is_our_side_slot(slot, hero) then return false end
return slot.status == "alive" or slot.status == "unknown"
end
function F.get_stack_security_priority(slot, radiant_mid_context, dire_mid_context)
if radiant_mid_context then
if slot.index == 26 then return 2 end
if slot.index == 6 then return -1 end
end
if dire_mid_context then
if slot.index == 5 then return -2 end
end
return 0
end
function F.get_stack_attack_prep_from_pos(pos, slot, move_speed, attacker)
if not move_speed or move_speed <= 0 then return 999 end
local attack_range = 150
local close_dist = math.max(F.get_stack_attack_target_distance(pos, slot) - attack_range, 0)
return (close_dist / move_speed) + F.get_unit_attack_point(attacker) + F.STACK_ATTACK_BUFFER
end
function F.infer_base_type(name)
local s = string.lower(name or "")
if s:find("ancient", 1, true) then return "Ancient", 3 end
if s:find("flooded", 1, true) and s:find("medium", 1, true) then return "Flooded Medium", 3 end
if s:find("flooded", 1, true) and s:find("small", 1, true) then return "Flooded Small", 2 end
if s:find("large", 1, true) or s:find("big", 1, true)
or s:find("hard", 1, true) then return "Big", 3 end
if s:find("medium", 1, true) then return "Medium", 2 end
if s:find("small", 1, true) or s:find("easy", 1, true) then return "Small", 1 end
return "Unknown", 2
end
function F.build_slot_id(index, pos)
return tostring(index)
end
function F.fill_slots_once(m)
if #m.slots > 0 then return end
local boxes = camp_boxes()
if not boxes then return end
m.slots = {}
m.slotsById = {}
for i, box in ipairs(boxes) do
local internal = box.GetField("neutralSpawnBoxes")
if internal then
local mins = internal.GetField("m_vMinBounds")
local maxs = internal.GetField("m_vMaxBounds")
if mins and maxs then
local center = (mins + maxs):Scaled(0.5)
local name = box.GetField("strCampName") or ("camp_" .. tostring(i))
local baseType, baseReq = F.infer_base_type(name)
local slot = {
id = F.build_slot_id(i, center), index = i, name = name,
center = center, mins = mins, maxs = maxs,
baseType = baseType, baseReq = baseReq,
predictedAlive = true, predictedRequired = baseReq,
predictedClass = baseType, predictedTTK = 6.5,
visible = false, centerVisible = false,
visibleAlive = false, visibleCreeps = {},
visibleCount = 0, visibleHp = 0,
lastSeenAlive = nil, nextRespawnAt = 0,
assigned = 0, status = "unknown",
reservedBy = nil, reservedCount = 0, reservedAt = 0,
lastClearTime = 0, lastFullCreepCount = nil,
learnedRequired = nil, learnedClass = nil,
learnedAt = 0, learnedTTK = nil,
bestRequiredEver = nil, lastVisibleRequired = nil,
lastRequired = baseReq,
knownCreepPositions = {},
}
slot.stackAggroTime = Camps.aggro_second(name)
m.slots[#m.slots + 1] = slot
m.slotsById[slot.id] = slot
end
end
end
m.debugState.slotCount = #m.slots
end
function F.spawn_check_points(slot)
if slot.knownCreepPositions and #slot.knownCreepPositions > 0 then
return slot.knownCreepPositions
end
local pts = { slot.center }
if slot.dormantPositions then
for _, sp in ipairs(slot.dormantPositions) do
pts[#pts + 1] = sp
end
end
return pts
end
function F.build_power_snapshot(m, hero)
local source = #m.units > 0 and m.units[1].entity or hero
return {
damage = F.get_true_damage(source),
aps = F.get_aps(source),
hp = F.get_health(source),
ehp = F.get_health(source) / math.max(F.get_armor_mult(source), 0.05),
}
end
function F.evaluate_visible_pack(slot, snapshot, hero)
if slot.visibleCount == 0 then return 99, "EMPTY", 999 end
local campEHP, campDPS, avgAM = 0, 0, 0
for _, creep in ipairs(slot.visibleCreeps) do
local hp = F.get_health(creep)
local am = F.get_armor_mult(creep)
campEHP = campEHP + hp / math.max(am, 0.05)
campDPS = campDPS + F.get_true_damage(creep) * F.get_aps(creep)
avgAM = avgAM + am
end
avgAM = avgAM / math.max(slot.visibleCount, 1)
local onePhysDPS = snapshot.damage * snapshot.aps * avgAM
local oneUnitDPS = math.max(onePhysDPS, 1)
local unitAM = snapshot.hp / math.max(snapshot.ehp, 1)
local incomingDPS = campDPS * unitAM
local oneUnitSurvival = snapshot.ehp / math.max(incomingDPS, 1)
for k = 1, 3 do
local total = oneUnitDPS * oneUnitSurvival * k * (k + 1) / 2
local ttk = campEHP / math.max(k * oneUnitDPS, 1)
if total >= campEHP * 1.20 then
local cls = k == 1 and "EASY_NOW" or k == 2 and "MEDIUM_NOW" or "HARD_NOW"
return k, cls, ttk
end
end
return 99, "UNSAFE", 999
end
function F.mark_slot_empty(slot)
slot.predictedAlive = false
slot.status = "empty"
slot.lastSeenAlive = false
slot._emptyConfirmSince = nil
slot._stackedCount = 0
slot._stackAttempts = 0
local clock = F.get_dota_clock()
slot.nextRespawnAt = clock >= 0 and ((math.floor(clock / 60) + 1) * 60) or 0
slot.lastClearTime = F.get_time()
slot.reservedBy = nil
slot.reservedCount = 0
slot.reservedAt = 0
end
function F.reset_camp_cycle(m)
local now = F.get_time()
local clock = F.get_dota_clock()
local any_respawned = false
for _, slot in ipairs(m.slots) do
if slot.status == "empty" and slot.nextRespawnAt > 0 and clock >= slot.nextRespawnAt then
slot.status = "unknown"
slot.lastSeenAlive = nil
slot.predictedAlive = true
slot.lastFullCreepCount = nil
slot.knownCreepPositions = {}
slot.reservedBy = nil
slot.reservedCount = 0
slot.assigned = 0
any_respawned = true
elseif slot.status == "in_progress" and slot.reservedAt > 0
and now - slot.reservedAt >= 18 then
slot.status = "unknown"
slot.reservedBy = nil
slot.reservedCount = 0
slot.assigned = 0
end
end
if any_respawned then
for _, u in ipairs(m.units) do
if u.taskType == "SLOT" and not u.taskEngaged then
u.taskType = "IDLE"
u.slotId = nil
end
end
m.forceRecalc = true
end
end
function F.get_jungle_state(m)
if not m._jungleState then
m._jungleState = F.read_jungle_state_from_module()
end
return m._jungleState
end
function F.vec_to_point(v)
if not v then return nil end
return { x = v:GetX(), y = v:GetY(), z = v:GetZ() }
end
function F.point_to_vec(p)
if not p or type(p) ~= "table" then return nil end
return Vector(p.x or 0, p.y or 0, p.z or 0)
end
function F.load_camp_positions(m)
if m._campPosLoaded then return end
m._campPosLoaded = true
local state = F.get_jungle_state(m)
for _, slot in ipairs(m.slots) do
local saved = state.campPositions[slot.id]
if saved and #saved > 0 and #slot.knownCreepPositions == 0 then
local pts = {}
for _, p in ipairs(saved) do
local v = F.point_to_vec(p)
if v then pts[#pts + 1] = v end
end
slot.knownCreepPositions = pts
end
end
end
function F.apply_stack_approaches(m)
if m._stackApproachesApplied then return end
m._stackApproachesApplied = true
for _, slot in ipairs(m.slots) do
local ap = F.STACK_APPROACH_POSITIONS[slot.index]
if ap and not slot._savedApproach then
slot._savedApproach = Vector(ap[1], ap[2], ap[3] or 0)
end
local pt = F.STACK_PULL_TARGETS[slot.index]
if pt and not slot._savedPullTarget then
slot._savedPullTarget = Vector(pt[1], pt[2], pt[3] or 0)
end
end
end
function F.load_pull_directions(m)
if m._pullDirsLoaded then return end
m._pullDirsLoaded = true
local state = F.get_jungle_state(m)
for _, slot in ipairs(m.slots) do
local pd = state.pullDirections[slot.id]
if pd then
local v = F.point_to_vec(pd)
if v then slot._pullDirCached = v end
end
end
end
function F.find_stack_edit_slot(m, pos)
local best, bd = nil, 999999
for _, slot in ipairs(m.slots) do
local d = F.dist(pos, slot.center)
if d < bd then bd = d; best = slot end
end
return best
end
function F.set_stack_approach_point(m, slot, pos)
if not slot then return end
slot._savedApproach = pos
m._pullMarkCampId = slot.id
F.save_stack_points(m)
end
function F.set_stack_pull_point(m, slot, pos)
if not slot then return end
slot._savedPullTarget = pos
F.save_stack_points(m)
end
function F.slot_is_safe(slot)
if not slot.predictedAlive then return false end
if slot.predictedRequired and slot.predictedRequired >= 99 then return false end
return true
end
function F.get_camp_difficulty(slot)
local manual = slot.manualDifficulty
if manual == "Small" then return "EASY" end
if manual == "Medium" then return "MEDIUM" end
if manual == "Big" then return "HARD" end
if manual == "Ancient" then return "ANCIENT" end
return "UNKNOWN"
end
function F.is_early_level(m)
return (m.heroState.level or 1) < 6
end
function F.find_nearest_alive_slot(m, from_pos, hero)
local best, bd = nil, 999999
for _, s in ipairs(m.slots) do
if s.predictedAlive and s.status ~= "empty" then
local d = F.dist(from_pos, s.center)
if d < bd then bd = d; best = s end
end
end
if not best then
for _, s in ipairs(m.slots) do
local d = F.dist(from_pos, s.center)
if d < bd and not F.is_near_fountain(s.center, hero) then
bd = d; best = s
end
end
end
return best
end
function F.count_existing_assignments(m, hero, hero_pos)
for _, u in ipairs(m.units) do
if u.taskType == "SLOT" and u.slotId then
local slot = m.slotsById[u.slotId]
if slot then
if slot.status == "empty" then
F.handle_camp_clear(u, slot, m, hero)
elseif F.is_camp_cleared(u, slot) then
F.handle_camp_clear(u, slot, m, hero)
else
slot.assigned = (slot.assigned or 0) + 1
slot.reservedCount = slot.assigned
end
else
u.taskType = "IDLE"; u.slotId = nil
end
end
end
end
function F.assign_early_gather(m, idle, hero_pos, hero)
if m.earlyGather.slotId then
local existing = m.slotsById[m.earlyGather.slotId]
if existing and existing.status ~= "empty" then
for _, u in ipairs(idle) do
u.taskType = "SLOT"
u.slotId = existing.id
u.taskClass = existing.predictedClass
u.taskEngaged = false
end
existing.assigned = (existing.assigned or 0) + #idle
existing.reservedCount = existing.assigned
return
end
end
local best_slot, best_score = nil, -1
for _, u in ipairs(idle) do
local cands = F.build_candidate_list(m, u, hero_pos, hero)
for _, c in ipairs(cands) do
if c.score > best_score then
best_score = c.score
best_slot = c.slot
end
break
end
end
if not best_slot then
m.earlyGather.slotId = nil
m.earlyGather.phase = nil
return
end
m.earlyGather.slotId = best_slot.id
m.earlyGather.phase = "GATHER"
m.earlyGather.attackAt = 0
best_slot.status = "in_progress"
best_slot.reservedBy = best_slot.id
best_slot.reservedAt = F.get_time()
for _, u in ipairs(idle) do
u.taskType = "SLOT"
u.slotId = best_slot.id
u.taskClass = best_slot.predictedClass
u.taskEngaged = false
end
best_slot.assigned = #idle
best_slot.reservedCount = #idle
end
function F.get_exit_pos(unit_pos, slot, pad)
pad = pad or 200
local cx = (slot.mins:GetX() + slot.maxs:GetX()) * 0.5
local cy = (slot.mins:GetY() + slot.maxs:GetY()) * 0.5
local dx = unit_pos:GetX() - cx
local dy = unit_pos:GetY() - cy
if math.abs(dx) < 1 and math.abs(dy) < 1 then dx = 1 end
local half_w = (slot.maxs:GetX() - slot.mins:GetX()) * 0.5
local half_h = (slot.maxs:GetY() - slot.mins:GetY()) * 0.5
local tx = math.abs(dx) > 0.01 and (half_w / math.abs(dx)) or 9999
local ty = math.abs(dy) > 0.01 and (half_h / math.abs(dy)) or 9999
local t_edge = math.min(tx, ty)
local exit_x = cx + dx * t_edge + (dx > 0 and pad or -pad)
local exit_y = cy + dy * t_edge + (dy > 0 and pad or -pad)
return Vector(exit_x, exit_y, unit_pos:GetZ())
end
function F.update_stack_stats(m)
if not m.stackStats then return end
local player = Players.GetLocal()
if not player then return end
local now = F.get_time()
local stacked = Player.GetCampsStacked(player) or 0
local ss = m.stackStats
ss.currentStacked = stacked
ss.samples = ss.samples or {}
local samples = ss.samples
local last = samples[#samples]
if not last or last.count ~= stacked or now >= (ss.nextSampleAt or 0) then
samples[#samples + 1] = { time = now, count = stacked }
ss.nextSampleAt = now + 1.0
end
local cutoff = now - 60.0
while #samples > 2 and samples[2].time <= cutoff do
table.remove(samples, 1)
end
local baseline = stacked
if samples[1] then baseline = samples[1].count end
for i = #samples, 1, -1 do
if samples[i].time <= cutoff then
baseline = samples[i].count
break
end
end
ss.stackedDelta60 = stacked - baseline
end
return F
end
M.NAMES = {
"get_time",
"is_near_fountain",
"get_true_damage",
"get_aps",
"get_armor_mult",
"get_health",
"get_unit_lifetime",
"get_unit_attack_point",
"get_stack_travel_cache_key",
"get_stack_candidate_travel_time",
"get_shortest_box_exit",
"get_best_pull_direction",
"get_stack_pull_target",
"get_stack_base_approach",
"get_stack_stage_pos",
"get_stack_attack_second",
"get_stack_timing_safety",
"get_stack_timing_difficulty",
"get_seconds_until_next_minute",
"get_stack_forecast_creep_exit_time",
"get_stack_forecast_pull_survival_time",
"get_stack_forecast_attack_deadline",
"is_ancient_slot",
"is_press_now_stack_candidate",
"get_stack_security_priority",
"get_stack_attack_prep_from_pos",
"infer_base_type",
"build_slot_id",
"fill_slots_once",
"spawn_check_points",
"build_power_snapshot",
"evaluate_visible_pack",
"mark_slot_empty",
"reset_camp_cycle",
"get_jungle_state",
"vec_to_point",
"point_to_vec",
"load_camp_positions",
"apply_stack_approaches",
"load_pull_directions",
"find_stack_edit_slot",
"set_stack_approach_point",
"set_stack_pull_point",
"slot_is_safe",
"get_camp_difficulty",
"is_early_level",
"find_nearest_alive_slot",
"count_existing_assignments",
"assign_early_gather",
"get_exit_pos",
"update_stack_stats",
}
return M
data/naga.lua386 lines
-- Persistent Naga TEA stack data.
return {
campDifficulties = {
["-1513_-3458"] = "Big",
["-1962_-4741"] = "Medium",
["-2480_-8400"] = "Big",
["-2569_3856"] = "Medium",
["-2964_7412"] = "Small",
["-3940_4945"] = "Small",
["-4032_895"] = "Big",
["-4240_8256"] = "Big",
["-4797_-104"] = "Ancient",
["-4854_3991"] = "Big",
["-766_4912"] = "Ancient",
["-769_-7685"] = "Big",
["-7983_-1783"] = "Medium",
["-8261_-432"] = "Big",
["1088_2536"] = "Big",
["1152_4096"] = "Medium",
["1831_-4034"] = "Medium",
["192_-5154"] = "Ancient",
["2079_7896"] = "Big",
["2671_-8371"] = "Small",
["307_7710"] = "Big",
["3392_-1216"] = "Big",
["4045_-5044"] = "Small",
["4099_63"] = "Ancient",
["4399_-8291"] = "Medium",
["4750_-3904"] = "Big",
["7937_-57"] = "Big",
["8448_1216"] = "Medium",
},
campPositions = {
["1"] = "[{'x': -759, 'y': 4876, 'z': 134}, {'x': -841, 'y': 5018, 'z': 134}]",
["11"] = "[{'x': -1948, 'y': -4823, 'z': 128}, {'x': -2004, 'y': -4800, 'z': 128}, {'x': -1881, 'y': -4859, 'z': 128}, {'x': -2076, 'y': -4801, 'z': 128}]",
["12"] = "[{'x': 1261, 'y': 4191, 'z': 128}, {'x': 1187, 'y': 4181, 'z': 128}]",
["14"] = "[{'x': -768, 'y': -7723, 'z': 134}, {'x': -707, 'y': -7650, 'z': 134}, {'x': -763, 'y': -7607, 'z': 134}]",
["15"] = "[{'x': 331, 'y': 7737, 'z': 134}, {'x': 371, 'y': 7690, 'z': 134}, {'x': 291, 'y': 7669, 'z': 134}]",
["16"] = "[{'x': 1054, 'y': 2616, 'z': 128}, {'x': 1066, 'y': 2517, 'z': 128}]",
["17"] = "[{'x': 8481, 'y': 1248, 'z': 256}, {'x': 8428, 'y': 1267, 'z': 256}, {'x': 8363, 'y': 1261, 'z': 256}]",
["18"] = "[{'x': -8351, 'y': -556, 'z': 256}, {'x': -8293, 'y': -500, 'z': 256}, {'x': -8291, 'y': -582, 'z': 256}]",
["19"] = "[{'x': -2778, 'y': 7312, 'z': 0}, {'x': -2904, 'y': 7310, 'z': 0}, {'x': -2799, 'y': 7364, 'z': 0}]",
["2"] = "[{'x': 3424, 'y': -1440, 'z': 256}, {'x': 3336, 'y': -1408, 'z': 256}]",
["20"] = "[{'x': -2569, 'y': 3769, 'z': 256}, {'x': -2584, 'y': 3863, 'z': 256}]",
["21"] = "[{'x': 2810, 'y': -8348, 'z': 0}, {'x': 2738, 'y': -8359, 'z': 0}]",
["22"] = "[{'x': 1985, 'y': -3951, 'z': 256}, {'x': 1937, 'y': -3896, 'z': 256}, {'x': 1917, 'y': -3956, 'z': 256}]",
["23"] = "[{'x': -3864, 'y': 4771, 'z': 128}, {'x': -3871, 'y': 4884, 'z': 128}, {'x': -3918, 'y': 4816, 'z': 128}]",
["24"] = "[{'x': -8036, 'y': -1868, 'z': 256}, {'x': -8005, 'y': -1771, 'z': 256}, {'x': -7978, 'y': -1848, 'z': 256}]",
["25"] = "[{'x': 2036, 'y': 7871, 'z': 128}, {'x': 1974, 'y': 7937, 'z': 128}, {'x': 1969, 'y': 7862, 'z': 128}, {'x': 2052, 'y': 7938, 'z': 128}]",
["26"] = "[{'x': -2469, 'y': -8419, 'z': 128}, {'x': -2373, 'y': -8396, 'z': 128}, {'x': -2468, 'y': -8475, 'z': 128}]",
["27"] = "[{'x': 4425, 'y': -8401, 'z': 0}, {'x': 4374, 'y': -8464, 'z': 0}, {'x': 4320, 'y': -8480, 'z': 0}]",
["28"] = "[{'x': -4208, 'y': 8314, 'z': 0}, {'x': -4131, 'y': 8356, 'z': 0}]",
["3"] = "[{'x': 3952, 'y': -4910, 'z': 128}, {'x': 4035, 'y': -4977, 'z': 128}, {'x': 3978, 'y': -4971, 'z': 128}]",
["4"] = "[{'x': 8005, 'y': -83, 'z': 256}, {'x': 7897, 'y': -139, 'z': 256}]",
["5"] = "[{'x': -4830, 'y': 3919, 'z': 128}, {'x': -4776, 'y': 3892, 'z': 128}, {'x': -4886, 'y': 3876, 'z': 128}]",
["6"] = "[{'x': 4652, 'y': -3704, 'z': 128}]",
["7"] = "[{'x': -1404, 'y': -3378, 'z': 128}, {'x': -1412, 'y': -3299, 'z': 128}, {'x': -1487, 'y': -3360, 'z': 128}]",
["8"] = "[{'x': 143, 'y': -5146, 'z': 134}, {'x': 219, 'y': -5197, 'z': 134}, {'x': 266, 'y': -5228, 'z': 134}]",
["9"] = "[{'x': -4958, 'y': -162, 'z': 256}, {'x': -5017, 'y': -94, 'z': 256}, {'x': -5070, 'y': -146, 'z': 256}]",
},
pullDirections = {
["26"] = {
edge = 368,
v = 3,
x = 6.1232342629258e-17,
y = 1,
},
["27"] = {
edge = 327.4375,
v = 3,
x = -1,
y = 1.2246468525852e-16,
},
["6"] = {
edge = 371.40283203125,
v = 3,
x = 1,
y = 0,
},
["8"] = {
edge = 377.7548828125,
v = 3,
x = 6.1232342629258e-17,
y = 1,
},
["9"] = {
edge = 488.5,
v = 3,
x = 6.1232342629258e-17,
y = 1,
},
},
stackPoints = {
approaches = {
["1"] = {
x = -747,
y = 4512,
z = 134,
},
["10"] = {
x = 4103.84375,
y = -189.34375,
z = 256,
},
["11"] = {
x = -1655.03125,
y = -4675,
z = 128,
},
["12"] = {
x = 861.09161376953,
y = 4220.21875,
z = 128,
},
["13"] = {
x = -3703.9362792969,
y = 788.12390136719,
z = 256,
},
["14"] = {
x = -559.53125,
y = -7998.9375,
z = 134,
},
["15"] = {
x = 402.21875,
y = 7480.5625,
z = 134,
},
["16"] = {
x = 1221.71875,
y = 2857.90625,
z = 128,
},
["17"] = {
x = 8172,
y = 1241,
z = 256,
},
["18"] = {
x = -7978,
y = -283,
z = 256,
},
["19"] = {
x = -3233,
y = 7685,
z = 0,
},
["2"] = {
x = 3126.25,
y = -1200.90625,
z = 256,
},
["20"] = {
x = -2608.8125,
y = 4164.59375,
z = 256,
},
["21"] = {
x = 3000.90625,
y = -8133.5625,
z = 0,
},
["22"] = {
x = 1877.5,
y = -4268.5,
z = 256,
},
["23"] = {
x = -4251.4375,
y = 4913.4375,
z = 128,
},
["24"] = {
x = -7864,
y = -1527,
z = 256,
},
["25"] = {
x = 2114.75,
y = 7552.875,
z = 147.15625,
},
["26"] = {
x = -2453.8125,
y = -8156.25,
z = 136.65625,
},
["27"] = {
x = 4548.25,
y = -8210.53125,
z = 0,
},
["28"] = {
x = -4079,
y = 7940,
z = 0,
},
["3"] = {
x = 4096.90625,
y = -5375.65625,
z = 128,
},
["4"] = {
x = 8211.6875,
y = -169.31373596191,
z = 256,
},
["5"] = {
x = -5036.4072265625,
y = 4196.0634765625,
z = 128,
},
["6"] = {
x = 4481.28125,
y = -4087.09375,
z = 128,
},
["7"] = {
x = -1222.71875,
y = -3755.46875,
z = 141.625,
},
["7_-4_-7"] = {
x = -1668,
y = -3791,
z = 128,
},
["8"] = {
x = 225.53125,
y = -4859,
z = 134,
},
["9"] = {
x = -4582.9375,
y = 193.09375,
z = 256,
},
},
pulls = {
["1"] = {
x = -705,
y = 3425,
z = 256,
},
["10"] = {
x = 3985.96875,
y = -1591,
z = 128,
},
["11"] = {
x = -546.84375,
y = -4735.5625,
z = 146.90625,
},
["12"] = {
x = 76.5625,
y = 4904.9375,
z = 128,
},
["13"] = {
x = -2392.6176757812,
y = 1217.3175048828,
z = 0,
},
["14"] = {
x = -96,
y = -8736,
z = 128,
},
["15"] = {
x = 622.09375,
y = 6537,
z = 128,
},
["16"] = {
x = 1605,
y = 3819,
z = 128,
},
["17"] = {
x = 7648,
y = 992,
z = 256,
},
["18"] = {
x = -7450,
y = 812,
z = 0,
},
["19"] = {
x = -3785,
y = 8187,
z = 0,
},
["2"] = {
x = 1838.65625,
y = -1421.75,
z = 0,
},
["20"] = {
x = -2592,
y = 5088,
z = 256,
},
["21"] = {
x = 3949.15625,
y = -8311.9375,
z = 1.1875,
},
["22"] = {
x = 1875.15625,
y = -5232.90625,
z = 256,
},
["23"] = {
x = -5374.4360351562,
y = 4929.5395507812,
z = 128,
},
["24"] = {
x = -7520,
y = -608,
z = 256,
},
["25"] = {
x = 2217.65625,
y = 6612.15625,
z = 128,
},
["26"] = {
x = -2592,
y = -7136,
z = 128,
},
["27"] = {
x = 5152,
y = -7648,
z = 0,
},
["28"] = {
x = -3808,
y = 7264,
z = 80,
},
["3"] = {
x = 4300.71875,
y = -6611.59375,
z = 128,
},
["4"] = {
x = 8736,
y = -1120,
z = 0,
},
["5"] = {
x = -5527.400390625,
y = 4734.5009765625,
z = 128,
},
["6"] = {
x = 2799.1875,
y = -5118.875,
z = 256,
},
["7"] = {
x = -514.90625,
y = -4388.09375,
z = 134,
},
["8"] = {
x = 352,
y = -3488,
z = 256,
},
["9"] = {
x = -4976,
y = 1535,
z = 128,
},
},
},
version = 1,
}
data/jungle.lua12 lines
-- Generated by Jungle TEA. Persistent stack data.
return {
campDifficulties = {},
campPositions = {},
pullDirections = {},
stackPoints = {
approaches = {},
pulls = {},
},
version = 1,
}
data/pull_timings.lua38 lines
-- GENERATED from the installed client's scripts/creep_pull_timings.txt (pak01_dir.vpk).
-- regenerate: py tools/farmplan/gen_pull_timings.py. read it through lib/camps
-- per spawn-box name: the second of the minute the stack pull starts and ends, and the
-- world yaw (degrees) to walk the aggroed camp out along. valve's own ALT overlay numbers.
return {
["neutralcamp_evil_1"] = { start = 54, stop = 56, dir = 40 },
["neutralcamp_evil_11"] = { start = 54, stop = 56, dir = 230 },
["neutralcamp_evil_12"] = { start = 55, stop = 57, dir = 265 },
["neutralcamp_evil_13"] = { start = 55, stop = 56, dir = 275 },
["neutralcamp_evil_14"] = { start = 55, stop = 56, dir = 345 },
["neutralcamp_evil_15"] = { start = 55, stop = 56, dir = 175 },
["neutralcamp_evil_2"] = { start = 55, stop = 57, dir = 225 },
["neutralcamp_evil_20"] = { start = 55, stop = 56, dir = 355 },
["neutralcamp_evil_3"] = { start = 53, stop = 55, dir = 140 },
["neutralcamp_evil_4"] = { start = 55, stop = 56, dir = 20 },
["neutralcamp_evil_5"] = { start = 55, stop = 56, dir = 160 },
["neutralcamp_evil_6"] = { start = 54, stop = 55, dir = 5 },
["neutralcamp_evil_7"] = { start = 55, stop = 56, dir = 40 },
["neutralcamp_evil_8"] = { start = 53, stop = 55, dir = 270 },
["neutralcamp_evil_9"] = { start = 53, stop = 55, dir = 180 },
["neutralcamp_good_1"] = { start = 55, stop = 57, dir = 145 },
["neutralcamp_good_11"] = { start = 55, stop = 57, dir = 30 },
["neutralcamp_good_12"] = { start = 53, stop = 55, dir = 185 },
["neutralcamp_good_13"] = { start = 55, stop = 56, dir = 115 },
["neutralcamp_good_14"] = { start = 55, stop = 57, dir = 100 },
["neutralcamp_good_15"] = { start = 56, stop = 57, dir = 345 },
["neutralcamp_good_16"] = { start = 55, stop = 56, dir = 160 },
["neutralcamp_good_2"] = { start = 52, stop = 54, dir = 320 },
["neutralcamp_good_20"] = { start = 55, stop = 56, dir = 200 },
["neutralcamp_good_3"] = { start = 53, stop = 55, dir = 340 },
["neutralcamp_good_4"] = { start = 54, stop = 55, dir = 170 },
["neutralcamp_good_5"] = { start = 55, stop = 56, dir = 350 },
["neutralcamp_good_6"] = { start = 55, stop = 57, dir = 240 },
["neutralcamp_good_7"] = { start = 53, stop = 55, dir = 350 },
["neutralcamp_good_8"] = { start = 52, stop = 54, dir = 120 },
["neutralcamp_good_9"] = { start = 56, stop = 57, dir = 100 },
}
No release notes recorded for this listing.
No published script needs it yet.
More from this author
ground
by hunnycGround height and high-ground level at any point of the dota map.
"requires": { "ground": "^1.0" }local ground = require("lib/ground")Beta Tester Config
by hunnycBeta 0.10.54 preset: combo on F, last hit on B, every other bind moved off dota's own keys, hp cards and minimap marks on.
lane_geom
by hunnycA lane as a polyline: the point on it nearest to a creep, the heading there, and a walk of N units along it.
"requires": { "lane_geom": "^1.0" }local lane_geom = require("lib/lane_geom")