map/mapTransportGraph.js

145 lines
4.6 KiB
JavaScript
Raw Normal View History

2026-05-28 15:48:42 +09:00
import { INF, MAP_H, MAP_W, MinHeap, indexOf, inside } from "./mapUtils.js";
export function buildCoarseCostGraph({ sea, costField }, options = {}) {
const scale = options.scale ?? 4;
const cw = Math.ceil(MAP_W / scale);
const ch = Math.ceil(MAP_H / scale);
const size = cw * ch;
const cost = new Float32Array(size);
const passable = new Uint8Array(size);
cost.fill(INF);
for (let cy = 0; cy < ch; cy++) {
for (let cx = 0; cx < cw; cx++) {
let best = INF;
let sum = 0;
let n = 0;
for (let dy = 0; dy < scale; dy++) {
for (let dx = 0; dx < scale; dx++) {
const x = cx * scale + dx;
const y = cy * scale + dy;
if (!inside(x, y)) continue;
const i = indexOf(x, y);
if (sea?.[i] || costField?.[i] >= INF) continue;
const v = costField[i];
best = Math.min(best, v);
sum += v;
n++;
}
}
const ci = cy * cw + cx;
if (n > 0) {
passable[ci] = 1;
cost[ci] = best * 0.55 + (sum / n) * 0.45;
}
}
}
return { scale, cw, ch, size, cost, passable };
}
export function routeCoarsePath(start, goal, graph, options = {}) {
if (!start || !goal || !graph) return [];
const sx = Math.floor(start.x / graph.scale);
const sy = Math.floor(start.y / graph.scale);
const gx = Math.floor(goal.x / graph.scale);
const gy = Math.floor(goal.y / graph.scale);
if (sx < 0 || sy < 0 || sx >= graph.cw || sy >= graph.ch || gx < 0 || gy < 0 || gx >= graph.cw || gy >= graph.ch) return [];
const startCi = sy * graph.cw + sx;
const goalCi = gy * graph.cw + gx;
if (!graph.passable[startCi] || !graph.passable[goalCi]) return [];
const score = new Float32Array(graph.size);
const cameFrom = new Int32Array(graph.size);
const closed = new Uint8Array(graph.size);
score.fill(INF);
cameFrom.fill(-1);
score[startCi] = 0;
const heap = new MinHeap();
heap.push({ i: startCi, f: Math.hypot(sx - gx, sy - gy) });
const maxExpanded = options.maxExpanded ?? graph.size;
let expanded = 0;
let found = -1;
while (heap.length && expanded++ < maxExpanded) {
const cur = heap.pop();
if (!cur || closed[cur.i]) continue;
closed[cur.i] = 1;
if (cur.i === goalCi) {
found = cur.i;
break;
}
const cx = cur.i % graph.cw;
const cy = Math.floor(cur.i / graph.cw);
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
const nx = cx + dx;
const ny = cy + dy;
if (nx < 0 || ny < 0 || nx >= graph.cw || ny >= graph.ch) continue;
const ni = ny * graph.cw + nx;
if (closed[ni] || !graph.passable[ni]) continue;
const nd = score[cur.i] + graph.cost[ni] * Math.hypot(dx, dy);
if (nd < score[ni]) {
score[ni] = nd;
cameFrom[ni] = cur.i;
heap.push({ i: ni, f: nd + Math.hypot(nx - gx, ny - gy) * (options.heuristicWeight ?? 0.85) });
}
}
}
}
if (found < 0) return [];
const path = [];
for (let p = found; p >= 0; p = cameFrom[p]) {
const cx = p % graph.cw;
const cy = Math.floor(p / graph.cw);
path.push([
Math.min(MAP_W - 1, Math.round(cx * graph.scale + graph.scale * 0.5)),
Math.min(MAP_H - 1, Math.round(cy * graph.scale + graph.scale * 0.5)),
]);
if (p === startCi) break;
}
return path.reverse();
}
export function refineCoarsePath(path, costField, options = {}) {
if (!path || path.length < 2) return [];
const sea = options.sea || null;
const radius = options.snapRadius ?? 1;
const out = [];
let lastKey = "";
for (let k = 0; k < path.length - 1; k++) {
const a = path[k];
const b = path[k + 1];
const steps = Math.max(1, Math.ceil(Math.hypot(b[0] - a[0], b[1] - a[1])));
for (let s = k === 0 ? 0 : 1; s <= steps; s++) {
const t = s / steps;
const tx = Math.round(a[0] + (b[0] - a[0]) * t);
const ty = Math.round(a[1] + (b[1] - a[1]) * t);
let best = null;
let bestScore = INF;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const x = tx + dx;
const y = ty + dy;
if (!inside(x, y)) continue;
const i = indexOf(x, y);
if (sea?.[i] || costField?.[i] >= INF) continue;
const score = costField[i] + Math.hypot(dx, dy) * 0.22;
if (score < bestScore) {
bestScore = score;
best = [x, y];
}
}
}
if (!best) return [];
const key = `${best[0]},${best[1]}`;
if (key !== lastKey) {
out.push(best);
lastKey = key;
}
}
}
return out.length >= 2 ? out : [];
}