163 lines
4.7 KiB
Lua
163 lines
4.7 KiB
Lua
local deque = require "lib.utils.deque"
|
||
local SQRT2 = math.sqrt(2)
|
||
|
||
local function isWalkable(point)
|
||
-- 1) В пределах уровня
|
||
if point.x < 0 or point.y < 0 then return false end
|
||
if point.x >= Tree.level.size.x or point.y >= Tree.level.size.y then return false end
|
||
|
||
-- 2) Клетка не занята персонажем
|
||
if Tree.level.characterGrid:get(point) then return false end
|
||
|
||
-- 3) Клетка проходима по тайлу
|
||
local tile = Tree.level.tileGrid:get(point)
|
||
if not tile or not tile.walkable then return false end
|
||
|
||
return true
|
||
end
|
||
|
||
--- @param a Vec3
|
||
--- @param b Vec3
|
||
--- @return number
|
||
local function heuristic(a, b)
|
||
local dx = math.abs(a.x - b.x)
|
||
local dy = math.abs(a.y - b.y)
|
||
return (dx + dy) + (SQRT2 - 2) * math.min(dx, dy)
|
||
end
|
||
|
||
--- @param from Vec3
|
||
--- @param to Vec3
|
||
--- @return boolean
|
||
local function can_step(from, to)
|
||
if not isWalkable(to) then return false end
|
||
|
||
local dx = to.x - from.x
|
||
local dy = to.y - from.y
|
||
if dx ~= 0 and dy ~= 0 then
|
||
-- нельзя просачиваться через диагональ
|
||
local viaX = Vec3 { from.x + dx, from.y }
|
||
local viaY = Vec3 { from.x, from.y + dy }
|
||
if not (isWalkable(viaX) or isWalkable(viaY)) then
|
||
return false
|
||
end
|
||
end
|
||
return true
|
||
end
|
||
|
||
--- @param cur Vec3
|
||
--- @return Vec3[]
|
||
local function neighbors(cur)
|
||
local res = {}
|
||
for dx = -1, 1 do
|
||
for dy = -1, 1 do
|
||
if not (dx == 0 and dy == 0) then
|
||
local nxt = Vec3 { cur.x + dx, cur.y + dy }
|
||
if can_step(cur, nxt) then
|
||
res[#res + 1] = nxt
|
||
end
|
||
end
|
||
end
|
||
end
|
||
return res
|
||
end
|
||
|
||
--- Восстановление пути (включая начало и цель)
|
||
--- @param cameFrom table<string, Vec3|nil>
|
||
--- @param goal Vec3
|
||
--- @return Deque
|
||
local function reconstruct_path(cameFrom, goal)
|
||
local sequence = {}
|
||
local cur = goal
|
||
while cur do
|
||
sequence[#sequence + 1] = cur
|
||
cur = cameFrom[tostring(cur)]
|
||
end
|
||
local acc = deque.new()
|
||
for i = 1, #sequence, 1 do
|
||
acc = acc:push_front(sequence[i])
|
||
end
|
||
return acc
|
||
end
|
||
|
||
--- @param openSet table множество вершин на границе
|
||
--- @param closed table уже обработанные
|
||
--- @param cameFrom table путь
|
||
--- @param gScore table
|
||
--- @param fScore table
|
||
--- @param goal Vec3
|
||
--- @return Deque
|
||
local function a_star_step(openSet, closed, cameFrom, gScore, fScore, goal)
|
||
-- пусто: пути нет
|
||
local anyKey = next(openSet)
|
||
if not anyKey then
|
||
return deque.new()
|
||
end
|
||
|
||
-- выбрать узел с минимальным fScore
|
||
local currentKey, currentNode = anyKey, openSet[anyKey]
|
||
local bestF = fScore[anyKey] or math.huge
|
||
for k, node in pairs(openSet) do
|
||
local f = fScore[k] or math.huge
|
||
if f < bestF then
|
||
bestF = f
|
||
currentKey, currentNode = k, node
|
||
end
|
||
end
|
||
|
||
-- достигли цели
|
||
if currentNode == goal then
|
||
return reconstruct_path(cameFrom, currentNode)
|
||
end
|
||
|
||
openSet[currentKey] = nil
|
||
closed[currentKey] = true
|
||
|
||
local gCurrent = gScore[currentKey] or math.huge
|
||
for _, nb in ipairs(neighbors(currentNode)) do
|
||
local nk = tostring(nb)
|
||
if not closed[nk] then
|
||
local dx = nb.x - currentNode.x
|
||
local dy = nb.y - currentNode.y
|
||
local step = (dx ~= 0 and dy ~= 0) and SQRT2 or 1 -- по диагонали ходить "дороже"
|
||
local tentativeG = gCurrent + step
|
||
if tentativeG < (gScore[nk] or math.huge) then
|
||
cameFrom[nk] = currentNode
|
||
gScore[nk] = tentativeG
|
||
fScore[nk] = tentativeG + heuristic(nb, goal)
|
||
if not openSet[nk] then
|
||
openSet[nk] = nb
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
return a_star_step(openSet, closed, cameFrom, gScore, fScore, goal)
|
||
end
|
||
|
||
--- @param from Vec3
|
||
--- @param to Vec3
|
||
--- @return Deque
|
||
local function trace(from, to)
|
||
-- не считаем путь до непроходимой точки (ибо нефиг)
|
||
if not isWalkable(to) then return deque.new():push_back(from) end
|
||
-- тривиальный случай
|
||
if from == to then
|
||
return deque.new():push_back(from)
|
||
end
|
||
|
||
local openSet = {}
|
||
local closed = {}
|
||
local cameFrom = {}
|
||
local gScore = {}
|
||
local fScore = {}
|
||
|
||
local sk = tostring(from)
|
||
openSet[sk] = from
|
||
gScore[sk] = 0
|
||
fScore[sk] = heuristic(from, to)
|
||
|
||
return a_star_step(openSet, closed, cameFrom, gScore, fScore, to)
|
||
end
|
||
|
||
return trace
|