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

pathing

Может ли юнит дойти из одной точки мира в другую: проходимость по GridNav с кэшем на каждого вызывающего, бюджетом путей на тик и стенами арены Марса.

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

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

API
pathing.build(deps: pathing.Deps): pathing.Query#

Собирает набор запросов проходимости поверх GridNav. Каждый вызов возвращает новые замыкания, две сборки ничего не делят.

Параметры
deps pathing.Deps настройки, часы, необязательные стены Марса
Возвращает
pathing.Query

Каждый запрос принимает необязательную таблицу кэша m: без неё ничего не кэшируется и бюджет не ограничен.

local P = require("lib/pathing").build({ C = C, U = { now = GameRules.GetGameTime } })
if P.can_reach_world_pos(Entity.GetAbsOrigin(me), target) then ... end

pathing.Deps

Поля
C: tableнастройки. обязательные: COG_AIM_ARRIVE_TOLERANCE, COG_PATH_QUICK_REACH_DIST, COG_PATH_CACHE_TTL, COG_PATH_STICKY_MAX; необязательные: COG_LOCAL_NAV_CACHE_TTL, COG_LOCAL_REACH_CACHE_TTL, COG_LOCAL_NAV_RADIUS_CELLS, COG_LOCAL_NAV_MAX_STEP_Z (README.md)
U: { now: fun(): number }часы, по которым стареют ответы в кэше
MarsArena?: tableстены арены Марса, pos_inside_wall(pos, team) и segment_crosses_wall(from, to, team); nil их не учитывает

pathing.Result

Поля
reachable: boolean?false, когда пути нет; nil, когда бюджет путей отложил запрос
clear: booleanпрямой проход, без обхода
endpoint: Vectorгде кончается проход: to или последняя точка неполного пути
walk_dist: number?длина пути в мировых единицах, когда точка достижима
local_nav: boolean?ответ дала локальная заливка вокруг from, без GridNav.GetPath
deferred: boolean?бюджет кончился, спросите на следующем тике

pathing.Cache

Поля
cog_push: tableгде библиотека держит состояние; вызывающий задаёт _tick_t, _path_budget, _path_budget_reserve, _path_cache, _walk_cache (README.md, «the cache table»)

pathing.Query

Что возвращает build: запросы, замкнутые на один набор deps.

Query.grid_coords(grid_pos: Vector): integer?, integer?#

Целые x, y точки сетки.

Параметры
grid_pos Vector точка сетки GridNav
Возвращает
integer?, integer? nil, когда grid_pos не вектор
Query.path_cache_key(from_pos: Vector, to_pos: Vector): string?#

Ключ, под которым кэшируется запрос между двумя мировыми точками: "fx:fy->tx:ty" в клетках сетки.

Параметры
from_pos Vector
to_pos Vector
Возвращает
string? nil, когда точка вне сетки
Query.is_world_walkable(pos: Vector, m: pathing.Cache?): boolean#

Проходима ли клетка сетки под мировой точкой.

Параметры
pos Vector
m pathing.Cache?
Возвращает
boolean true вне сетки; false при nil pos
Query.quick_reach_check(from_pos: Vector, to_pos: Vector, m: pathing.Cache?): boolean#

Проходит ли прямой путь без GridNav.GetPath: оба конца проходимы, между ними нет стены или обрыва, расстояние меньше COG_PATH_QUICK_REACH_DIST.

Параметры
from_pos Vector
to_pos Vector
m pathing.Cache?
Возвращает
boolean false значит «не напрямую», а не «недостижимо»
Query.cog_path_query(from_pos: Vector, to_pos: Vector, m: pathing.Cache?): pathing.Result#

Полный ответ о проходе между двумя мировыми точками: достижима ли цель, напрямую ли, где кончается путь и какой он длины.

Параметры
from_pos Vector
to_pos Vector
m pathing.Cache?
Возвращает
pathing.Result

Пробует кэш, затем прямой проход, затем локальную заливку, затем GridNav.GetPath, пока хватает бюджета.

Query.can_reach_world_pos(from_pos: Vector, to_pos: Vector, m: pathing.Cache?): boolean?#

Может ли юнит из from_pos дойти до to_pos.

Параметры
from_pos Vector
to_pos Vector
m pathing.Cache?
Возвращает
boolean? nil, когда бюджет путей отложил запрос
Query.can_reach_grid_pos(from_grid: Vector, to_grid: Vector, m: pathing.Cache?): boolean?#

То же, что can_reach_world_pos, для двух точек сетки GridNav.

Параметры
from_grid Vector
to_grid Vector
m pathing.Cache?
Возвращает
boolean? nil, когда запрос отложен; true, когда любая из точек nil
Query.path_budget_available(m: pathing.Cache?): boolean#

Можно ли ещё вызвать GridNav.GetPath на этом тике (_path_budget больше _path_budget_reserve).

Параметры
m pathing.Cache?
Возвращает
boolean true, когда m не задаёт бюджет
Query.note_path_scan_deferred(m: pathing.Cache?)#

Помечает в m, что запрос отложен, чтобы вызывающий спросил снова на следующем тике.

Параметры
m pathing.Cache?
Query.clear_local_reach_cache(m: pathing.Cache?)#

Сбрасывает локальную заливку, следующий запрос зальёт заново от своей стартовой клетки.

Параметры
m pathing.Cache?
Query.debug_local_reach_cache(m: pathing.Cache?, from_pos: Vector, radius: integer?): table?#

Заливает проходимые клетки вокруг from_pos и возвращает заливку, чтобы её нарисовать.

Параметры
m pathing.Cache?
from_pos Vector
radius integer? клетки, по умолчанию COG_LOCAL_NAV_RADIUS_CELLS
Возвращает
table? { cells = { ["x:y"] = true } }, nil вне сетки
Описание

lib/pathing

Может ли юнит дойти из одной точки мира в другую: проходимость по GridNav с кэшем на каждого вызывающего, бюджетом путей на тик и стенами арены Марса. Вынесена из uTEA/heroes/clockwerk/rules/pathing (#132) без изменений; ею пользуются clockwerk и dodger.

local P = require("lib/pathing").build({
    U = { now = function() return GameRules.GetGameTime() end },
    MarsArena = require("uTEA/core/mars_arena_walls"),   -- optional
    C = {
        COG_AIM_ARRIVE_TOLERANCE = 32,    -- closer than this counts as arrived
        COG_PATH_QUICK_REACH_DIST = 650,  -- straight-line check only below this
        COG_PATH_CACHE_TTL = 0.35,        -- seconds a cached answer lives
        COG_PATH_STICKY_MAX = 128,        -- cached answers before pruning
        -- optional, defaults in brackets: COG_LOCAL_NAV_CACHE_TTL (1.0),
        -- COG_LOCAL_REACH_CACHE_TTL (1.0), COG_LOCAL_NAV_RADIUS_CELLS (24),
        -- COG_LOCAL_NAV_MAX_STEP_Z (96)
    },
})
local ok = P.can_reach_world_pos(from, to, m)   -- true, false, or nil when deferred

build возвращает замыкания; две сборки ничего не делят.

таблица кэша m

Каждый запрос принимает необязательную m. Без неё ничего не кэшируется и бюджет не ограничен. С ней состояние живёт в m.cog_push (имя осталось от clockwerk и сохранится в 1.x):

поле кто задаёт смысл
_path_cache вызывающий, каждый тик ответы этого тика
_path_sticky библиотека ответы, которые живут COG_PATH_CACHE_TTL
_tick_t вызывающий время вместо U.now()
_path_budget, _path_budget_reserve вызывающий сколько вызовов GridNav.GetPath можно на этом тике
_path_scan_deferred библиотека запрос упёрся в бюджет и ответил nil
_walk_cache, _local_nav_* вызывающий / библиотека проходимые клетки и локальная заливка

api

функция возвращает
cog_path_query(from, to, m) { reachable, clear, endpoint, walk_dist, local_nav, deferred }
can_reach_world_pos(from, to, m) reachable, nil, когда бюджет отложил запрос
can_reach_grid_pos(from_grid, to_grid, m) то же для точек сетки
quick_reach_check(from, to, m) true, когда проходит прямой путь, без GetPath
is_world_walkable(pos, m) клетка под pos проходима
path_budget_available(m), note_path_scan_deferred(m) состояние бюджета
clear_local_reach_cache(m), debug_local_reach_cache(m, from, radius) локальная заливка
grid_coords(grid), path_cache_key(from, to) помощники для ключей вызывающих

что нужно

GridNav, Heroes, Entity из compat/legacy_misc.lua (его грузит utea.lua:84) и нативный Vector. Скрипт вне utea сначала грузит прослойки compat; переход на нативный api gridnav будет в 2.0.

тест

tools\lua\lua.exe scripts\lib\pathing\selftest.lua из корня репозитория: GridNav-заглушка, старый путь require, который возвращает этот модуль, и ответы кэша, бюджета и стен арены.

Исходник

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

init.lua621 строк
---@diagnostic disable: undefined-global
-- lib/pathing: gridnav reachability between world points, cached per caller. moved out of
-- uTEA/heroes/clockwerk/rules/pathing (#132) unchanged; api and deps in README.md.
--
-- Owns: path cache on m.cog_push when m provided (the name is clockwerk's, kept for 1.x)
-- Must not: cast abilities or emit intents.

local Pathing = {}

---@class pathing.Deps
---@field C table tunables. required: COG_AIM_ARRIVE_TOLERANCE, COG_PATH_QUICK_REACH_DIST, COG_PATH_CACHE_TTL, COG_PATH_STICKY_MAX; optional: COG_LOCAL_NAV_CACHE_TTL, COG_LOCAL_REACH_CACHE_TTL, COG_LOCAL_NAV_RADIUS_CELLS, COG_LOCAL_NAV_MAX_STEP_Z (README.md)
---@field U { now: fun(): number } the clock cached answers age by
---@field MarsArena? table mars arena walls, `pos_inside_wall(pos, team)` and `segment_crosses_wall(from, to, team)`; nil ignores them

---@class pathing.Result
---@field reachable boolean? false when no path; nil when the path budget deferred the query
---@field clear boolean a straight walk, no detour
---@field endpoint Vector where the walk ends: `to`, or the last point of a partial path
---@field walk_dist number? path length in world units, when reachable
---@field local_nav boolean? answered by the local flood around `from`, without GridNav.GetPath
---@field deferred boolean? the budget ran out; ask again next tick

---@class pathing.Cache
---@field cog_push table where the library keeps its state; the caller sets `_tick_t`, `_path_budget`, `_path_budget_reserve`, `_path_cache`, `_walk_cache` (README.md "the cache table")

---Builds a reachability query set over GridNav. Each call returns fresh closures; two builds share nothing.
---Every query takes an optional cache table `m`: without it nothing is cached and the budget is unlimited.
---```lua
---local P = require("lib/pathing").build({ C = C, U = { now = GameRules.GetGameTime } })
---if P.can_reach_world_pos(Entity.GetAbsOrigin(me), target) then ... end
---```
---@param deps pathing.Deps tunables, clock, optional mars walls
---@return pathing.Query
function Pathing.build(deps)
    local C = deps.C
    local MarsArena = deps.MarsArena
    local U = deps.U

    local COG_AIM_ARRIVE_TOLERANCE = C.COG_AIM_ARRIVE_TOLERANCE
    local COG_PATH_QUICK_REACH_DIST = C.COG_PATH_QUICK_REACH_DIST
    local COG_PATH_CACHE_TTL = C.COG_PATH_CACHE_TTL
    local COG_PATH_STICKY_MAX = C.COG_PATH_STICKY_MAX
    local COG_LOCAL_NAV_CACHE_TTL = C.COG_LOCAL_NAV_CACHE_TTL or 1.0
    local COG_LOCAL_REACH_CACHE_TTL = C.COG_LOCAL_REACH_CACHE_TTL or 1.0
    local COG_LOCAL_NAV_RADIUS_CELLS = C.COG_LOCAL_NAV_RADIUS_CELLS or 24
    local COG_LOCAL_NAV_MAX_STEP_Z = C.COG_LOCAL_NAV_MAX_STEP_Z or 96

    local function vector_coords(pos_or_grid)
        if not pos_or_grid or not pos_or_grid.GetX or not pos_or_grid.GetY then return nil end
        return math.floor(pos_or_grid:GetX()), math.floor(pos_or_grid:GetY())
    end

    ---Integer x, y of a grid point.
    ---@param grid_pos Vector a GridNav grid point
    ---@return integer?, integer? # nil when `grid_pos` is not a vector
    local function grid_coords(grid_pos)
        return vector_coords(grid_pos)
    end

    local function world_grid_coords(world_pos)
        if not world_pos then return nil end
        local g = GridNav.WorldToGrid(world_pos)
        if not g then return nil end
        return vector_coords(g)
    end

    local function cache_key_from_coords(fx, fy, tx, ty)
        if not fx or not tx then return nil end
        return string.format("%d:%d->%d:%d", fx, fy, tx, ty)
    end

    ---The key a query between two world points is cached under: "fx:fy->tx:ty" in grid cells.
    ---@param from_pos Vector
    ---@param to_pos Vector
    ---@return string? # nil when a point is off the grid
    local function path_cache_key(from_pos, to_pos)
        local fx, fy = world_grid_coords(from_pos)
        local tx, ty = world_grid_coords(to_pos)
        return cache_key_from_coords(fx, fy, tx, ty)
    end

    local function grid_path_cache_key(from_grid, to_grid)
        local fx, fy = grid_coords(from_grid)
        local tx, ty = grid_coords(to_grid)
        return cache_key_from_coords(fx, fy, tx, ty)
    end

    local function local_team_for_walk()
        local lp = Heroes and Heroes.GetLocal and Heroes.GetLocal() or nil
        if lp and Entity.GetTeamNum then return Entity.GetTeamNum(lp) end
        return nil
    end

    local function pos_blocked_by_mars_wall(pos)
        if not MarsArena then return false end
        local team = local_team_for_walk()
        return MarsArena.pos_inside_wall(pos, team) and true or false
    end

    local function prune_path_sticky(sticky, t)
        if not sticky then return end
        local count = 0
        for _ in pairs(sticky) do count = count + 1 end
        if count <= COG_PATH_STICKY_MAX then return end
        for key, entry in pairs(sticky) do
            if t - (entry.t or 0) >= COG_PATH_CACHE_TTL then
                sticky[key] = nil
            end
        end
    end

    local function lookup_path_entry(m, key)
        if not key or not m or not m.cog_push then return nil end
        local tick_cache = m.cog_push._path_cache
        if tick_cache and tick_cache[key] ~= nil then return tick_cache[key] end
        local sticky = m.cog_push._path_sticky
        if sticky then
            local entry = sticky[key]
            local t = m.cog_push._tick_t or U.now()
            if entry and t - (entry.t or 0) < COG_PATH_CACHE_TTL then
                if tick_cache then tick_cache[key] = entry end
                return entry
            end
        end
        return nil
    end

    local function store_path_entry(m, key, entry)
        if not key or not entry or not m or not m.cog_push then return end
        if m.cog_push._path_cache then m.cog_push._path_cache[key] = entry end
        local sticky = m.cog_push._path_sticky
        if not sticky then sticky = {}; m.cog_push._path_sticky = sticky end
        entry.t = m.cog_push._tick_t or U.now()
        sticky[key] = entry
        prune_path_sticky(sticky, entry.t)
    end

    ---Whether a GridNav.GetPath call is still allowed this tick (`_path_budget` above `_path_budget_reserve`).
    ---@param m pathing.Cache?
    ---@return boolean # true when `m` sets no budget
    local function path_budget_available(m)
        if not m or not m.cog_push or m.cog_push._path_budget == nil then return true end
        local reserve = m.cog_push._path_budget_reserve or 0
        return m.cog_push._path_budget > reserve
    end

    local function consume_path_budget(m)
        if not m or not m.cog_push or m.cog_push._path_budget == nil then return true end
        if not path_budget_available(m) then return false end
        m.cog_push._path_budget = m.cog_push._path_budget - 1
        return true
    end

    ---Marks `m` as having deferred a query, so the caller knows to ask again next tick.
    ---@param m pathing.Cache?
    local function note_path_scan_deferred(m)
        if m and m.cog_push then m.cog_push._path_scan_deferred = true end
    end

    local function is_walkable_node(node)
        return node and (node & 0x103) == 1
    end

    local function grid_key(gx, gy)
        return tostring(gx) .. ":" .. tostring(gy)
    end

    local function query_grid_world_pos(gx, gy)
        if not GridNav or not GridNav.GridToWorld then return nil end
        if GridNav.GetGridIndex and GridNav.GetGridPoint then
            local idx = GridNav.GetGridIndex(gx, gy)
            if idx then
                local gp = GridNav.GetGridPoint(idx)
                if gp then
                    local wp = GridNav.GridToWorld(gp)
                    if wp then return wp end
                end
            end
        end
        return GridNav.GridToWorld(Vector(gx, gy, 0))
    end

    local function cached_grid_world_pos(m, gx, gy)
        if not gx or not gy then return nil end
        local now = m and m.cog_push and (m.cog_push._tick_t or U.now()) or U.now()
        local cache = nil
        if m and m.cog_push then
            cache = m.cog_push._local_nav_world
            if not cache then
                cache = {}
                m.cog_push._local_nav_world = cache
            end
        end
        if cache then
            local key = grid_key(gx, gy)
            local entry = cache[key]
            if entry and now - (entry.t or 0) < COG_LOCAL_NAV_CACHE_TTL then
                return entry.pos
            end
            local pos = query_grid_world_pos(gx, gy)
            cache[key] = { pos = pos, t = now }
            return pos
        end
        return query_grid_world_pos(gx, gy)
    end

    local function grid_step_height_clear(m, ax, ay, bx, by)
        if COG_LOCAL_NAV_MAX_STEP_Z <= 0 then return true end
        local a = cached_grid_world_pos(m, ax, ay)
        local b = cached_grid_world_pos(m, bx, by)
        if not a or not b then return true end
        return math.abs((a:GetZ() or 0) - (b:GetZ() or 0)) <= COG_LOCAL_NAV_MAX_STEP_Z
    end

    local function segment_height_clear(from_pos, to_pos, m)
        if COG_LOCAL_NAV_MAX_STEP_Z <= 0 or not from_pos or not to_pos then return true end
        local last_x, last_y = world_grid_coords(from_pos)
        if not last_x then return true end
        for i = 1, 8 do
            local t = i / 8
            local p = Vector(
                from_pos:GetX() + (to_pos:GetX() - from_pos:GetX()) * t,
                from_pos:GetY() + (to_pos:GetY() - from_pos:GetY()) * t,
                from_pos:GetZ())
            local gx, gy = world_grid_coords(p)
            if gx and (gx ~= last_x or gy ~= last_y) then
                if not grid_step_height_clear(m, last_x, last_y, gx, gy) then
                    return false
                end
                last_x, last_y = gx, gy
            end
        end
        return true
    end

    local function cached_grid_walkable(m, gx, gy)
        if not gx or not gy then return false end
        local now = m and m.cog_push and (m.cog_push._tick_t or U.now()) or U.now()
        local cache = nil
        if m and m.cog_push then
            cache = m.cog_push._local_nav_nodes
            if not cache then
                cache = {}
                m.cog_push._local_nav_nodes = cache
            end
            if now - (m.cog_push._local_nav_prune_t or 0) >= COG_LOCAL_NAV_CACHE_TTL then
                for key, entry in pairs(cache) do
                    if now - (entry.t or 0) >= COG_LOCAL_NAV_CACHE_TTL * 2 then
                        cache[key] = nil
                    end
                end
                local world_cache = m.cog_push._local_nav_world
                if world_cache then
                    for key, entry in pairs(world_cache) do
                        if now - (entry.t or 0) >= COG_LOCAL_NAV_CACHE_TTL * 2 then
                            world_cache[key] = nil
                        end
                    end
                end
                m.cog_push._local_nav_prune_t = now
            end
        end
        if cache then
            local key = grid_key(gx, gy)
            local entry = cache[key]
            if entry and now - (entry.t or 0) < COG_LOCAL_NAV_CACHE_TTL then
                return entry.walkable
            end
            local node = GridNav.GetNode(gx, gy)
            local walkable = is_walkable_node(node)
            cache[key] = { walkable = walkable, t = now }
            return walkable
        end
        return is_walkable_node(GridNav.GetNode(gx, gy))
    end

    ---Drops the local reach flood, so the next query floods again from its own start cell.
    ---@param m pathing.Cache?
    local function clear_local_reach_cache(m)
        if m and m.cog_push then m.cog_push._local_reach_cache = nil end
    end

    local LOCAL_REACH_NEIGHBORS = {
        {  1,  0 }, { -1,  0 }, {  0,  1 }, {  0, -1 },
        {  1,  1 }, {  1, -1 }, { -1,  1 }, { -1, -1 },
    }

    local function build_local_reach_cache(m, sx, sy, radius)
        if not (m and m.cog_push and sx and sy) then return nil end
        radius = radius or COG_LOCAL_NAV_RADIUS_CELLS
        local now = m.cog_push._tick_t or U.now()
        local existing = m.cog_push._local_reach_cache
        if existing
            and existing.sx == sx
            and existing.sy == sy
            and (existing.radius or 0) >= radius
            and now - (existing.t or 0) < COG_LOCAL_REACH_CACHE_TTL then
            return existing
        end

        m.cog_push._local_nav_nodes = m.cog_push._local_nav_nodes or {}

        local reach = {
            key = string.format("%d:%d:%d", sx, sy, radius),
            t = now,
            sx = sx,
            sy = sy,
            radius = radius,
            cells = {},
        }
        m.cog_push._local_reach_cache = reach

        if not cached_grid_walkable(m, sx, sy) then return reach end

        local min_x, max_x = sx - radius, sx + radius
        local min_y, max_y = sy - radius, sy + radius
        local qx, qy, head, tail = { sx }, { sy }, 1, 1
        reach.cells[grid_key(sx, sy)] = true

        while head <= tail do
            local x, y = qx[head], qy[head]
            head = head + 1

            for _, step in ipairs(LOCAL_REACH_NEIGHBORS) do
                local nx, ny = x + step[1], y + step[2]
                if nx >= min_x and nx <= max_x and ny >= min_y and ny <= max_y then
                    local nkey = grid_key(nx, ny)
                    if not reach.cells[nkey] then
                        local diagonal = step[1] ~= 0 and step[2] ~= 0
                        local corner_clear = true
                        if diagonal then
                            corner_clear = cached_grid_walkable(m, x + step[1], y)
                                and cached_grid_walkable(m, x, y + step[2])
                                and grid_step_height_clear(m, x, y, x + step[1], y)
                                and grid_step_height_clear(m, x, y, x, y + step[2])
                                and grid_step_height_clear(m, x + step[1], y, nx, ny)
                                and grid_step_height_clear(m, x, y + step[2], nx, ny)
                        end
                        if corner_clear
                            and grid_step_height_clear(m, x, y, nx, ny)
                            and cached_grid_walkable(m, nx, ny) then
                            reach.cells[nkey] = true
                            tail = tail + 1
                            qx[tail], qy[tail] = nx, ny
                        end
                    end
                end
            end
        end

        return reach
    end

    local function local_reach_between_cells(m, sx, sy, tx, ty)
        if not (m and m.cog_push and sx and sy and tx and ty) then return nil end
        if not GridNav or not GridNav.GetNode then return nil end

        local radius = COG_LOCAL_NAV_RADIUS_CELLS
        if math.abs(tx - sx) > radius or math.abs(ty - sy) > radius then return nil end

        local reach = build_local_reach_cache(m, sx, sy, radius)
        if not reach then return nil end
        return reach.cells[grid_key(tx, ty)] == true
    end

    local function local_reach_check_world(from_pos, to_pos, m)
        if not (from_pos and to_pos) then return nil end
        local sx, sy = world_grid_coords(from_pos)
        local tx, ty = world_grid_coords(to_pos)
        if MarsArena then
            local team = local_team_for_walk()
            if pos_blocked_by_mars_wall(to_pos) then return false end
            if MarsArena.segment_crosses_wall(from_pos, to_pos, team) then return false end
        end
        return local_reach_between_cells(m, sx, sy, tx, ty)
    end

    ---Floods walkable cells around `from_pos` and returns the flood, for drawing it.
    ---@param m pathing.Cache?
    ---@param from_pos Vector
    ---@param radius integer? cells, COG_LOCAL_NAV_RADIUS_CELLS by default
    ---@return table? # `{ cells = { ["x:y"] = true } }`, nil off the grid
    local function debug_local_reach_cache(m, from_pos, radius)
        if not from_pos then return nil end
        local sx, sy = world_grid_coords(from_pos)
        if not sx then return nil end
        return build_local_reach_cache(m, sx, sy, radius)
    end

    local function local_reach_check_grid(from_grid, to_grid, m)
        local sx, sy = grid_coords(from_grid)
        local tx, ty = grid_coords(to_grid)
        return local_reach_between_cells(m, sx, sy, tx, ty)
    end

    local function path_last_world(from_pos, path)
        local last_world = from_pos
        if not path or #path < 2 then return last_world end
        for i = 2, #path do
            local gp = GridNav.GetGridPoint(path[i])
            if gp then last_world = gp end
        end
        return last_world
    end

    local function path_walk_dist(from_pos, to_pos, path, is_clear)
        if not (from_pos and to_pos) then return nil end
        if is_clear then return (from_pos - to_pos):Length2D() end
        if not path or #path == 0 then return nil end

        local prev = from_pos
        local dist = 0
        for i = 1, #path do
            local gp = GridNav.GetGridPoint(path[i])
            local p = nil
            if gp and gp.GetX and GridNav and GridNav.GridToWorld then
                p = GridNav.GridToWorld(gp)
            elseif gp and gp.GetX then
                p = gp
            end
            if p then
                local step = (p - prev):Length2D()
                if step > 0 and step < 2048 then
                    dist = dist + step
                    prev = p
                end
            end
        end

        local last = (to_pos - prev):Length2D()
        if last > 0 and last < 2048 then dist = dist + last end
        if dist <= 1 then return (from_pos - to_pos):Length2D() end
        return dist
    end

    ---Whether the grid cell under a world point is walkable.
    ---@param pos Vector
    ---@param m pathing.Cache?
    ---@return boolean # true off the grid; false for nil `pos`
    local function is_world_walkable(pos, m)
        if not pos then return false end
        local gx, gy = world_grid_coords(pos)
        if not gx then return true end
        local key = gx .. ":" .. gy
        local cache = m and m.cog_push and m.cog_push._walk_cache
        if cache and cache[key] ~= nil then return cache[key] end
        local ok = cached_grid_walkable(m, gx, gy)
        if cache then cache[key] = ok end
        return ok
    end

    ---Whether a straight walk works, without GridNav.GetPath: both ends walkable, no wall or cliff between, closer than COG_PATH_QUICK_REACH_DIST.
    ---@param from_pos Vector
    ---@param to_pos Vector
    ---@param m pathing.Cache?
    ---@return boolean # false means "not straight", not "unreachable"
    local function quick_reach_check(from_pos, to_pos, m)
        if not from_pos or not to_pos then return false end
        local dist = (from_pos - to_pos):Length2D()
        if dist <= COG_AIM_ARRIVE_TOLERANCE then return true end
        if not is_world_walkable(from_pos, m) or not is_world_walkable(to_pos, m) then return false end
        if MarsArena then
            local team = local_team_for_walk()
            if pos_blocked_by_mars_wall(to_pos) then return false end
            if MarsArena.segment_crosses_wall(from_pos, to_pos, team) then return false end
        end
        if not segment_height_clear(from_pos, to_pos, m) then return false end
        if dist > COG_PATH_QUICK_REACH_DIST then return false end
        if GridNav.IsLineClear then
            if GridNav.IsLineClear(from_pos, to_pos) then return true end
        end
        for i = 1, 3 do
            local t = i / 4
            local mid = Vector(
                from_pos:GetX() + (to_pos:GetX() - from_pos:GetX()) * t,
                from_pos:GetY() + (to_pos:GetY() - from_pos:GetY()) * t,
                from_pos:GetZ())
            if not is_world_walkable(mid, m) then return false end
        end
        return true
    end

    ---The full answer for a walk between two world points: reachable, straight or not, where it ends and how long it is.
    ---Tries the cache, then a straight walk, then the local flood, then GridNav.GetPath while the budget lasts.
    ---@param from_pos Vector
    ---@param to_pos Vector
    ---@param m pathing.Cache?
    ---@return pathing.Result
    local function cog_path_query(from_pos, to_pos, m)
        if not from_pos or not to_pos then
            return { reachable = false, clear = false, endpoint = from_pos }
        end
        if (from_pos - to_pos):Length2D() <= COG_AIM_ARRIVE_TOLERANCE then
            return { reachable = true, clear = true, endpoint = to_pos }
        end
        local key = path_cache_key(from_pos, to_pos)
        local cached = lookup_path_entry(m, key)
        if cached then return cached end
        if quick_reach_check(from_pos, to_pos, m) then
            local entry = {
                reachable = true,
                clear = true,
                endpoint = to_pos,
                walk_dist = (from_pos - to_pos):Length2D(),
            }
            store_path_entry(m, key, entry)
            return entry
        end
        local local_reach = local_reach_check_world(from_pos, to_pos, m)
        if local_reach == true then
            local entry = {
                reachable = true,
                clear = false,
                endpoint = to_pos,
                local_nav = true,
                walk_dist = (from_pos - to_pos):Length2D(),
            }
            store_path_entry(m, key, entry)
            return entry
        end
        if not path_budget_available(m) then
            note_path_scan_deferred(m)
            return { reachable = nil, clear = false, endpoint = from_pos, deferred = true }
        end
        local gs = GridNav.WorldToGrid(from_pos)
        local ge = GridNav.WorldToGrid(to_pos)
        if not gs or not ge then
            local entry = { reachable = true, clear = true, endpoint = to_pos }
            store_path_entry(m, key, entry)
            return entry
        end
        consume_path_budget(m)
        local path, is_clear = GridNav.GetPath(gs, ge)
        local reachable = path and #path > 0 or false
        if reachable and MarsArena and pos_blocked_by_mars_wall(to_pos) then
            reachable = false
            is_clear = false
        end
        if reachable and MarsArena then
            local team = local_team_for_walk()
            if MarsArena.segment_crosses_wall(from_pos, to_pos, team) then
                reachable = false
                is_clear = false
            end
        end
        local entry = {
            reachable = reachable,
            clear = is_clear and reachable,
            endpoint = is_clear and to_pos or path_last_world(from_pos, path),
            walk_dist = reachable and path_walk_dist(from_pos, to_pos, path, is_clear) or nil,
        }
        store_path_entry(m, key, entry)
        return entry
    end

    ---Whether a unit at `from_pos` can walk to `to_pos`.
    ---@param from_pos Vector
    ---@param to_pos Vector
    ---@param m pathing.Cache?
    ---@return boolean? # nil when the path budget deferred the query
    local function can_reach_world_pos(from_pos, to_pos, m)
        local result = cog_path_query(from_pos, to_pos, m)
        if result.deferred then return nil end
        return result.reachable
    end

    ---Same as `can_reach_world_pos`, for two GridNav grid points.
    ---@param from_grid Vector
    ---@param to_grid Vector
    ---@param m pathing.Cache?
    ---@return boolean? # nil when deferred; true when either point is nil
    local function can_reach_grid_pos(from_grid, to_grid, m)
        if not from_grid or not to_grid then return true end
        local key = grid_path_cache_key(from_grid, to_grid)
        local cached = lookup_path_entry(m, key)
        if cached then
            if cached.deferred then return nil end
            return cached.reachable
        end
        local local_reach = local_reach_check_grid(from_grid, to_grid, m)
        if local_reach == true then
            local entry = {
                reachable = true,
                clear = false,
                endpoint = to_grid,
                local_nav = true,
            }
            store_path_entry(m, key, entry)
            return true
        end
        if not path_budget_available(m) then
            note_path_scan_deferred(m)
            return nil
        end
        consume_path_budget(m)
        local path = GridNav.GetPath(from_grid, to_grid)
        local reachable = path and #path > 0 or false
        local entry = { reachable = reachable, clear = reachable, endpoint = reachable and to_grid or from_grid }
        store_path_entry(m, key, entry)
        return reachable
    end

    ---@class pathing.Query
    ---What `build` returns: the queries, closed over one set of deps.
    return {
        grid_coords = grid_coords,
        path_cache_key = path_cache_key,
        is_world_walkable = is_world_walkable,
        quick_reach_check = quick_reach_check,
        cog_path_query = cog_path_query,
        can_reach_world_pos = can_reach_world_pos,
        can_reach_grid_pos = can_reach_grid_pos,
        path_budget_available = path_budget_available,
        note_path_scan_deferred = note_path_scan_deferred,
        clear_local_reach_cache = clear_local_reach_cache,
        debug_local_reach_cache = debug_local_reach_cache,
    }
end

return Pathing
pack.json2 строк
{ "kind": "lib", "version": "1.0.1" }
Изменения

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

Используют

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

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

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

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