/**
* Поиск пути A* по тайловой сетке.
* Движение 4-стороннее; диагонали опциональны (с запретом «среза углов» через блокеры).
*/
export interface Grid {
width: number;
height: number;
/** Проходим ли тайл. */
isWalkable(x: number, y: number): boolean;
}
interface Node {
x: number;
y: number;
g: number;
f: number;
parent: Node | null;
}
function heuristic(a: { x: number; y: number }, b: { x: number; y: number }): number {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}
/**
* Возвращает путь (без стартового тайла, включая конечный) или null, если пути нет.
* Включение диагоналей добавляет шаги (±1, ±1), но только если оба ортогональных
* соседа проходимы (без среза углов).
*/
export function findPath(
grid: Grid,
start: { x: number; y: number },
goal: { x: number; y: number },
allowDiagonal = false
): { x: number; y: number }[] | null {
if (!grid.isWalkable(goal.x, goal.y)) return null;
if (start.x === goal.x && start.y === goal.y) return [];
const key = (x: number, y: number) => y * grid.width + x;
const open: Node[] = [];
const best = new Map<number, number>();
const startNode: Node = { x: start.x, y: start.y, g: 0, f: heuristic(start, goal), parent: null };
open.push(startNode);
best.set(key(start.x, start.y), 0);
const dirs4 = [
{ x: 1, y: 0 },
{ x: -1, y: 0 },
{ x: 0, y: 1 },
{ x: 0, y: -1 }
];
while (open.length > 0) {
// Извлекаем узел с минимальным f (карты маленькие, линейного поиска достаточно).
let idx = 0;
for (let i = 1; i < open.length; i++) {
if (open[i].f < open[idx].f) idx = i;
}
const cur = open.splice(idx, 1)[0];
if (cur.x === goal.x && cur.y === goal.y) {
const path: { x: number; y: number }[] = [];
for (let n: Node | null = cur; n !== null; n = n.parent) {
path.unshift({ x: n.x, y: n.y });
}
path.shift(); // убираем стартовый тайл
return path;
}
const neighbors = allowDiagonal
? [...dirs4, { x: 1, y: 1 }, { x: 1, y: -1 }, { x: -1, y: 1 }, { x: -1, y: -1 }]
: dirs4;
for (const d of neighbors) {
const nx = cur.x + d.x;
const ny = cur.y + d.y;
if (nx < 0 || ny < 0 || nx >= grid.width || ny >= grid.height) continue;
if (!grid.isWalkable(nx, ny)) continue;
if (d.x !== 0 && d.y !== 0) {
// Диагональ разрешена только без среза углов.
if (!grid.isWalkable(cur.x + d.x, cur.y) || !grid.isWalkable(cur.x, cur.y + d.y)) continue;
}
const g = cur.g + 1;
const k = key(nx, ny);
if (best.has(k) && best.get(k)! <= g) continue;
best.set(k, g);
open.push({ x: nx, y: ny, g, f: g + heuristic({ x: nx, y: ny }, goal), parent: cur });
}
}
return null;
}
/**
* Путь к ближайшей проходимой клетке, соседней с goal (8-соседство).
* Идём к краю цели: goal может быть непроходим (тайл NPC/объекта) или занят —
* стоять на нём не нужно. Пути нет — null.
*/
export function findPathToNeighbor(
grid: Grid,
start: { x: number; y: number },
goal: { x: number; y: number }
): { x: number; y: number }[] | null {
let best: { x: number; y: number }[] | null = null;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
const tx = goal.x + dx;
const ty = goal.y + dy;
if (tx < 0 || ty < 0 || tx >= grid.width || ty >= grid.height) continue;
if (!grid.isWalkable(tx, ty)) continue;
const p = findPath(grid, start, { x: tx, y: ty });
if (p && (best === null || p.length < best.length)) best = p;
}
}
return best;
}