evolve
商店/共享库

pathing

判断单位能否从世界中的一点走到另一点:基于 GridNav 的可达性检查,带按调用方区分的缓存、每 tick 的寻路预算和玛尔斯竞技场的墙。

hunnyc0 个脚本在用
支持源码公开
在脚本中使用
pack.json
"requires": { "pathing": "^1.0" }
你的 lua
local pathing = require("lib/pathing")

玩家无需安装:脚本会自动带上库,取范围内的任一版本。发布前可在开发会话中修改 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?预算已用完,下一 tick 再查询

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? grid_pos 不是向量时为 nil
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;pos 为 nil 时为 false
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;任一点为 nil 时为 true
Query.path_budget_available(m: pathing.Cache?): boolean#

本 tick 是否还允许调用 GridNav.GetPath(_path_budget 大于 _path_budget_reserve)。

参数
m pathing.Cache?
返回
boolean m 未设置预算时为 true
Query.note_path_scan_deferred(m: pathing.Cache?)#

在 m 中标记有查询被推迟,调用方据此在下一 tick 重新查询。

参数
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 的可达性检查,带按调用方区分的缓存、每 tick 的寻路预算和玛尔斯竞技场的墙。从 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 调用方,每 tick 本 tick 的结果
_path_sticky 库 保留 COG_PATH_CACHE_TTL 的结果
_tick_t 调用方 代替 U.now() 的时间
_path_budget, _path_budget_reserve 调用方 本 tick 允许调用 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) 不调用 GetPath,能直线走到时为 true
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) 调用方用来生成键的辅助函数

依赖

compat/legacy_misc.lua 中的 GridNav、Heroes、Entity(由 utea.lua:84 加载)和原生 Vector。utea 之外的脚本需先加载 compat 兼容层;改用原生 gridnav api 将在 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