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(); const gScore = new Map(); const fScore = new Map(); const startK = key(start.x, start.y); gScore.set(startK, 0); fScore.set(startK, manhattan(start, end)); const inOpen = new Set([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 []; }