105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
import type { World, Vec2 } from "../../core/types";
|
|
import { key } from "../../core/utils";
|
|
import { manhattan } from "../../core/math";
|
|
import { inBounds, isWall, isBlocked, idx } from "./world-logic";
|
|
import { type EntityManager } from "../EntityManager";
|
|
|
|
/**
|
|
* Simple 4-dir A* pathfinding.
|
|
* Returns an array of positions INCLUDING start and end. If no path, returns [].
|
|
*
|
|
* Exploration rule:
|
|
* - You cannot path THROUGH unseen tiles.
|
|
* - You cannot path TO an unseen target tile.
|
|
*/
|
|
export function findPathAStar(w: World, seen: Uint8Array, start: Vec2, end: Vec2, options: { ignoreBlockedTarget?: boolean; ignoreSeen?: boolean; em?: EntityManager } = {}): Vec2[] {
|
|
if (!inBounds(w, end.x, end.y)) return [];
|
|
if (isWall(w, end.x, end.y)) return [];
|
|
|
|
// If not ignoring target block, fail if blocked
|
|
if (!options.ignoreBlockedTarget && isBlocked(w, end.x, end.y, options.em)) return [];
|
|
|
|
if (!options.ignoreSeen && seen[idx(w, end.x, end.y)] !== 1) return [];
|
|
|
|
const open: Vec2[] = [start];
|
|
const cameFrom = new Map<string, string>();
|
|
|
|
const gScore = new Map<string, number>();
|
|
const fScore = new Map<string, number>();
|
|
|
|
const startK = key(start.x, start.y);
|
|
gScore.set(startK, 0);
|
|
fScore.set(startK, manhattan(start, end));
|
|
|
|
const inOpen = new Set<string>([startK]);
|
|
|
|
const dirs = [
|
|
{ x: 1, y: 0 },
|
|
{ x: -1, y: 0 },
|
|
{ x: 0, y: 1 },
|
|
{ x: 0, y: -1 }
|
|
];
|
|
|
|
while (open.length > 0) {
|
|
// Pick node with lowest fScore
|
|
let bestIdx = 0;
|
|
let bestF = Infinity;
|
|
for (let i = 0; i < open.length; i++) {
|
|
const k = key(open[i].x, open[i].y);
|
|
const f = fScore.get(k) ?? Infinity;
|
|
if (f < bestF) {
|
|
bestF = f;
|
|
bestIdx = i;
|
|
}
|
|
}
|
|
|
|
const current = open.splice(bestIdx, 1)[0];
|
|
const currentK = key(current.x, current.y);
|
|
inOpen.delete(currentK);
|
|
|
|
if (current.x === end.x && current.y === end.y) {
|
|
// Reconstruct path
|
|
const path: Vec2[] = [end];
|
|
let k = currentK;
|
|
while (cameFrom.has(k)) {
|
|
const prevK = cameFrom.get(k)!;
|
|
const [px, py] = prevK.split(",").map(Number);
|
|
path.push({ x: px, y: py });
|
|
k = prevK;
|
|
}
|
|
path.reverse();
|
|
return path;
|
|
}
|
|
|
|
for (const d of dirs) {
|
|
const nx = current.x + d.x;
|
|
const ny = current.y + d.y;
|
|
if (!inBounds(w, nx, ny)) continue;
|
|
if (isWall(w, nx, ny)) continue;
|
|
|
|
// Exploration rule: cannot path through unseen (except start, or if ignoreSeen is set)
|
|
if (!options.ignoreSeen && !(nx === start.x && ny === start.y) && seen[idx(w, nx, ny)] !== 1) continue;
|
|
|
|
// Avoid walking through other actors (except standing on start, OR if it is the target and we ignore block)
|
|
const isTarget = nx === end.x && ny === end.y;
|
|
if (!isTarget && !(nx === start.x && ny === start.y) && isBlocked(w, nx, ny, options.em)) continue;
|
|
|
|
const nK = key(nx, ny);
|
|
const tentativeG = (gScore.get(currentK) ?? Infinity) + 1;
|
|
|
|
if (tentativeG < (gScore.get(nK) ?? Infinity)) {
|
|
cameFrom.set(nK, currentK);
|
|
gScore.set(nK, tentativeG);
|
|
fScore.set(nK, tentativeG + manhattan({ x: nx, y: ny }, end));
|
|
|
|
if (!inOpen.has(nK)) {
|
|
open.push({ x: nx, y: ny });
|
|
inOpen.add(nK);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|