evolve
Магазин/Общие библиотеки

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

hunnyc0 скриптов используют
ПоддержкаИсходник открыт
Подключить в скрипт
pack.json
"requires": { "camps": "^1.0" }
твой lua
local camps = require("lib/camps")

Игрокам ставить ничего не нужно: скрипт приносит библиотеку сам, любую версию в пределах диапазона. Правку можно проверить до публикации в dev-сессии, в scripts/lib/camps/.

API
camps.pull_window(name)#
camps.aggro_second(name)#
camps.stack_state(name)#
camps.encode(state)#
camps.save_stack_state(name, state)#
camps.copy_state(state)#
camps.merge_missing(state, defaults)#
camps.install_stacking(F)#
Описание

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.

Исходник

Код версии 1.0.0, как его получает клиент. Байткод в сборке собран из этих файлов.

init.lua243 строк
---@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 строк
{ "kind": "lib", "version": "1.0.0" }
stacking.lua688 строк
---@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 строк
-- 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 строк
-- Generated by Jungle TEA. Persistent stack data.
return {
    campDifficulties = {},
    campPositions = {},
    pullDirections = {},
    stackPoints = {
        approaches = {},
        pulls = {},
    },
    version = 1,
}
data/pull_timings.lua38 строк
-- 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 },
}
Изменения

Заметок к версиям нет.

Используют

Пока ни один опубликованный скрипт её не требует.

Ещё у этого автора

Ещё у этого автора

ground

от hunnyc

Ground height and high-ground level at any point of the dota map.

pack.json
"requires": { "ground": "^1.0" }
твой lua
local ground = require("lib/ground")
0 скриптов используютОпубликованных версий пока нет

Beta Tester Config

от hunnyc
Входит в доступ

Beta 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.

configcombolast hit
Оценок пока нет0 установок
v1.0.0 · Обновлён 2026-10-17✓ 0.10.70

lane_geom

от hunnyc

A lane as a polyline: the point on it nearest to a creep, the heading there, and a walk of N units along it.

pack.json
"requires": { "lane_geom": "^1.0" }
твой lua
local lane_geom = require("lib/lane_geom")
0 скриптов используютv1.0.0