From 82d393a06474bb52345f34a7495a33ec489d959c Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 3 Sep 2025 00:28:46 +0300 Subject: [PATCH] - proper A* implementation - not really as I'm a freak - tiles are now walkable by default --- lib/level/tile.lua | 5 +- lib/pathfinder.lua | 164 ++++++++++++++++++++++++++++++++++++++++----- main.lua | 11 ++- 3 files changed, 160 insertions(+), 20 deletions(-) diff --git a/lib/level/tile.lua b/lib/level/tile.lua index 74afba4..8f494d3 100644 --- a/lib/level/tile.lua +++ b/lib/level/tile.lua @@ -19,7 +19,10 @@ end --- @class Tile --- @field atlasData TileAtlasData --- @field position Vec3 -local tile = {} +--- @field walkable boolean +local tile = { + walkable = true +} tile.__index = tile --- TODO: сделать как love.graphics.draw несколько сигнатур у функции diff --git a/lib/pathfinder.lua b/lib/pathfinder.lua index cf1ef9f..656be80 100644 --- a/lib/pathfinder.lua +++ b/lib/pathfinder.lua @@ -1,34 +1,162 @@ local deque = require "lib.utils.deque" +local SQRT2 = math.sqrt(2) ---- @param cur Vec3 +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 ---- @param acc Deque -local function greedy_trace_step(cur, to, acc) - local lengthTable = {} - for x = -1, 1 do - for y = -1, 1 do - local point = Vec3 { cur.x + x, cur.y + y } - table.insert(lengthTable, { point, (point - to):length() }) +--- @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 - local min = lengthTable[1] - for i = 2, #lengthTable do - if lengthTable[i][2] < min[2] then min = lengthTable[i] end - end - local next = min[1] + return true +end - acc = acc:push_back(cur) - if cur == to then - return acc +--- @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 greedy_trace_step(next, to, acc) + return res +end + +--- Восстановление пути (включая начало и цель) +--- @param cameFrom table +--- @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) - return greedy_trace_step(from, to, deque.new()) + -- не считаем путь до непроходимой точки (ибо нефиг) + 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 diff --git a/main.lua b/main.lua index 2577671..a4716f8 100644 --- a/main.lua +++ b/main.lua @@ -9,7 +9,16 @@ function love.conf(t) end function love.load() - character.spawn("Hero", "warrior", Tree.assets.files.sprites.character) + for x = 0, 29, 1 do + for y = 0, 29, 1 do + if math.random() > 0.8 then + local c = character.spawn("Hero", "warrior", Tree.assets.files.sprites.character) + c.logic.mapLogic.position = Vec3 { x, y } + c.logic.mapLogic.displayedPosition = Vec3 { x, y } + end + end + end + -- PlayerFaction.characters = { Hero1, Hero2 } love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true })