mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-18 17:53:29 +00:00
Pathfinding Refactor
This commit is contained in:
@@ -2,10 +2,13 @@ import { readdirSync, readFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { Game } from "../../../../src/core/game/Game.js";
|
||||
import { TileRef } from "../../../../src/core/game/GameMap.js";
|
||||
import { NavMesh } from "../../../../src/core/pathfinding/navmesh/NavMesh.js";
|
||||
import { AStarWaterHierarchical } from "../../../../src/core/pathfinding/algorithms/AStar.WaterHierarchical.js";
|
||||
import { setupFromPath } from "../../utils.js";
|
||||
|
||||
// Available comparison adapters
|
||||
// Note: "hpa" runs same algorithm without debug overhead for fair timing comparison
|
||||
export const COMPARISON_ADAPTERS = ["hpa", "a.baseline", "a.generic", "a.full"];
|
||||
|
||||
export interface MapInfo {
|
||||
name: string;
|
||||
displayName: string;
|
||||
@@ -13,7 +16,7 @@ export interface MapInfo {
|
||||
|
||||
export interface MapCache {
|
||||
game: Game;
|
||||
navMesh: NavMesh;
|
||||
hpaStar: AStarWaterHierarchical;
|
||||
}
|
||||
|
||||
const cache = new Map<string, MapCache>();
|
||||
@@ -114,13 +117,20 @@ export async function loadMap(mapName: string): Promise<MapCache> {
|
||||
const mapsDir = getMapsDirectory();
|
||||
|
||||
// Use the existing setupFromPath utility to load the map
|
||||
const game = await setupFromPath(mapsDir, mapName);
|
||||
const game = await setupFromPath(mapsDir, mapName, { disableNavMesh: false });
|
||||
|
||||
// Initialize NavMesh
|
||||
const navMesh = new NavMesh(game, { cachePaths: config.cachePaths });
|
||||
navMesh.initialize();
|
||||
// Get pre-built graph from game
|
||||
const graph = game.miniWaterGraph();
|
||||
if (!graph) {
|
||||
throw new Error(`No water graph available for map: ${mapName}`);
|
||||
}
|
||||
|
||||
const cacheEntry: MapCache = { game, navMesh };
|
||||
// Initialize AStarWaterHierarchical with minimap and graph
|
||||
const hpaStar = new AStarWaterHierarchical(game.miniMap(), graph, {
|
||||
cachePaths: config.cachePaths,
|
||||
});
|
||||
|
||||
const cacheEntry: MapCache = { game, hpaStar };
|
||||
|
||||
// Store in cache
|
||||
cache.set(mapName, cacheEntry);
|
||||
@@ -132,7 +142,7 @@ export async function loadMap(mapName: string): Promise<MapCache> {
|
||||
* Get map metadata for client
|
||||
*/
|
||||
export async function getMapMetadata(mapName: string) {
|
||||
const { game, navMesh } = await loadMap(mapName);
|
||||
const { game, hpaStar } = await loadMap(mapName);
|
||||
|
||||
// Extract map data
|
||||
const mapData: number[] = [];
|
||||
@@ -143,65 +153,48 @@ export async function getMapMetadata(mapName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract static graph data from NavMesh
|
||||
// Extract static graph data from GameMapHPAStar
|
||||
// Access internal graph via type casting (test code only)
|
||||
const graph = (hpaStar as any).graph;
|
||||
const miniMap = game.miniMap();
|
||||
const navMeshGraph = (navMesh as any).graph;
|
||||
|
||||
// Convert gateways from Map to array
|
||||
const gatewaysArray = Array.from(navMeshGraph.gateways.values());
|
||||
const allGateways = gatewaysArray.map((gw: any) => ({
|
||||
id: gw.id,
|
||||
x: miniMap.x(gw.tile),
|
||||
y: miniMap.y(gw.tile),
|
||||
// Convert nodes to client format
|
||||
const allNodes = graph.getAllNodes().map((node: any) => ({
|
||||
id: node.id,
|
||||
x: miniMap.x(node.tile),
|
||||
y: miniMap.y(node.tile),
|
||||
}));
|
||||
|
||||
// Create a lookup map from gateway ID to gateway for edge conversion
|
||||
const gatewayById = new Map(gatewaysArray.map((gw: any) => [gw.id, gw]));
|
||||
// Convert edges to client format
|
||||
const edges: Array<{
|
||||
fromId: number;
|
||||
toId: number;
|
||||
from: number[];
|
||||
to: number[];
|
||||
cost: number;
|
||||
}> = [];
|
||||
for (let i = 0; i < graph.edgeCount; i++) {
|
||||
const edge = graph.getEdge(i);
|
||||
if (!edge) continue;
|
||||
|
||||
// Convert edges from Map<gatewayId, Edge[]> to flat array
|
||||
// The edges Map has gateway IDs as keys, and arrays of edges as values
|
||||
const allEdges: any[] = [];
|
||||
for (const edgeArray of navMeshGraph.edges.values()) {
|
||||
allEdges.push(...edgeArray);
|
||||
const nodeA = graph.getNode(edge.nodeA);
|
||||
const nodeB = graph.getNode(edge.nodeB);
|
||||
if (!nodeA || !nodeB) continue;
|
||||
|
||||
edges.push({
|
||||
fromId: edge.nodeA,
|
||||
toId: edge.nodeB,
|
||||
from: [miniMap.x(nodeA.tile) * 2, miniMap.y(nodeA.tile) * 2],
|
||||
to: [miniMap.x(nodeB.tile) * 2, miniMap.y(nodeB.tile) * 2],
|
||||
cost: edge.cost,
|
||||
});
|
||||
}
|
||||
|
||||
// Deduplicate edges (they're bidirectional, so each edge appears twice)
|
||||
const seenEdges = new Set<string>();
|
||||
const edges = allEdges
|
||||
.filter((edge: any) => {
|
||||
const edgeKey =
|
||||
edge.from < edge.to
|
||||
? `${edge.from}-${edge.to}`
|
||||
: `${edge.to}-${edge.from}`;
|
||||
if (seenEdges.has(edgeKey)) return false;
|
||||
seenEdges.add(edgeKey);
|
||||
return true;
|
||||
})
|
||||
.map((edge: any) => {
|
||||
const fromGateway = gatewayById.get(edge.from);
|
||||
const toGateway = gatewayById.get(edge.to);
|
||||
|
||||
return {
|
||||
fromId: edge.from,
|
||||
toId: edge.to,
|
||||
from: fromGateway
|
||||
? [miniMap.x(fromGateway.tile) * 2, miniMap.y(fromGateway.tile) * 2]
|
||||
: [0, 0],
|
||||
to: toGateway
|
||||
? [miniMap.x(toGateway.tile) * 2, miniMap.y(toGateway.tile) * 2]
|
||||
: [0, 0],
|
||||
cost: edge.cost,
|
||||
path: edge.path
|
||||
? edge.path.map((tile: TileRef) => [game.x(tile), game.y(tile)])
|
||||
: null,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Map ${mapName}: ${allGateways.length} gateways, ${edges.length} edges`,
|
||||
`Map ${mapName}: ${allNodes.length} nodes, ${edges.length} edges`,
|
||||
);
|
||||
|
||||
const sectorSize = navMeshGraph.sectorSize;
|
||||
const clusterSize = graph.clusterSize;
|
||||
|
||||
return {
|
||||
name: mapName,
|
||||
@@ -209,10 +202,11 @@ export async function getMapMetadata(mapName: string) {
|
||||
height: game.height(),
|
||||
mapData,
|
||||
graphDebug: {
|
||||
allGateways,
|
||||
allNodes,
|
||||
edges,
|
||||
sectorSize,
|
||||
clusterSize,
|
||||
},
|
||||
adapters: COMPARISON_ADAPTERS,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,64 @@
|
||||
import { TileRef } from "../../../../src/core/game/GameMap.js";
|
||||
import { MiniAStarAdapter } from "../../../../src/core/pathfinding/adapters/MiniAStarAdapter.js";
|
||||
import { loadMap } from "./maps.js";
|
||||
import { AStarWaterHierarchical } from "../../../../src/core/pathfinding/algorithms/AStar.WaterHierarchical.js";
|
||||
import { BresenhamSmoothingTransformer } from "../../../../src/core/pathfinding/smoothing/BresenhamPathSmoother.js";
|
||||
import { ComponentCheckTransformer } from "../../../../src/core/pathfinding/transformers/ComponentCheckTransformer.js";
|
||||
import { MiniMapTransformer } from "../../../../src/core/pathfinding/transformers/MiniMapTransformer.js";
|
||||
import { ShoreCoercingTransformer } from "../../../../src/core/pathfinding/transformers/ShoreCoercingTransformer.js";
|
||||
import {
|
||||
PathFinder,
|
||||
SteppingPathFinder,
|
||||
} from "../../../../src/core/pathfinding/types.js";
|
||||
import { getAdapter } from "../../utils.js";
|
||||
import { COMPARISON_ADAPTERS, loadMap } from "./maps.js";
|
||||
|
||||
interface PathfindingOptions {
|
||||
includePfMini?: boolean;
|
||||
includeNavMesh?: boolean;
|
||||
}
|
||||
|
||||
interface NavMeshResult {
|
||||
// Primary result with debug info
|
||||
interface PrimaryResult {
|
||||
path: Array<[number, number]> | null;
|
||||
initialPath: Array<[number, number]> | null;
|
||||
gateways: Array<[number, number]> | null;
|
||||
timings: any;
|
||||
length: number;
|
||||
time: number;
|
||||
debug: {
|
||||
nodePath: Array<[number, number]> | null;
|
||||
initialPath: Array<[number, number]> | null;
|
||||
timings: Record<string, number>;
|
||||
};
|
||||
}
|
||||
|
||||
interface PfMiniResult {
|
||||
// Comparison result (path + timing only)
|
||||
interface ComparisonResult {
|
||||
adapter: string;
|
||||
path: Array<[number, number]> | null;
|
||||
length: number;
|
||||
time: number;
|
||||
}
|
||||
|
||||
// Cache pathfinding adapters per map
|
||||
const pfMiniCache = new Map<string, MiniAStarAdapter>();
|
||||
export interface PathfindResult {
|
||||
primary: PrimaryResult;
|
||||
comparisons: ComparisonResult[];
|
||||
}
|
||||
|
||||
// Cache adapters per map
|
||||
const adapterCache = new Map<
|
||||
string,
|
||||
Map<string, SteppingPathFinder<TileRef>>
|
||||
>();
|
||||
|
||||
/**
|
||||
* Get or create MiniAStar adapter for a map
|
||||
* Get or create an adapter for a map
|
||||
*/
|
||||
function getPfMiniAdapter(mapName: string, game: any): MiniAStarAdapter {
|
||||
if (!pfMiniCache.has(mapName)) {
|
||||
const adapter = new MiniAStarAdapter(game, { waterPath: true });
|
||||
pfMiniCache.set(mapName, adapter);
|
||||
function getOrCreateAdapter(
|
||||
mapName: string,
|
||||
adapterName: string,
|
||||
game: any,
|
||||
): SteppingPathFinder<TileRef> {
|
||||
if (!adapterCache.has(mapName)) {
|
||||
adapterCache.set(mapName, new Map());
|
||||
}
|
||||
return pfMiniCache.get(mapName)!;
|
||||
const mapAdapters = adapterCache.get(mapName)!;
|
||||
|
||||
if (!mapAdapters.has(adapterName)) {
|
||||
mapAdapters.set(adapterName, getAdapter(game, adapterName));
|
||||
}
|
||||
return mapAdapters.get(adapterName)!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,110 +73,177 @@ function pathToCoords(
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute pathfinding between two points
|
||||
* Build the full transformer chain like PathFinding.Water() does
|
||||
*/
|
||||
export async function computePath(
|
||||
mapName: string,
|
||||
from: [number, number],
|
||||
to: [number, number],
|
||||
options: PathfindingOptions = {},
|
||||
): Promise<NavMeshResult> {
|
||||
const { game, navMesh: navMeshAdapter } = await loadMap(mapName);
|
||||
function buildWrappedPathFinder(
|
||||
hpaStar: AStarWaterHierarchical,
|
||||
game: any,
|
||||
graph: any,
|
||||
): PathFinder<TileRef> {
|
||||
const miniMap = game.miniMap();
|
||||
const componentCheckFn = (t: TileRef) => graph.getComponentId(t);
|
||||
|
||||
// Convert coordinates to TileRefs
|
||||
const fromRef = game.ref(from[0], from[1]);
|
||||
const toRef = game.ref(to[0], to[1]);
|
||||
// Chain: hpaStar -> ComponentCheck -> Bresenham -> ShoreCoercing -> MiniMap
|
||||
const withComponentCheck = new ComponentCheckTransformer(
|
||||
hpaStar,
|
||||
componentCheckFn,
|
||||
);
|
||||
const withSmoothing = new BresenhamSmoothingTransformer(
|
||||
withComponentCheck,
|
||||
miniMap,
|
||||
);
|
||||
const withShoreCoercing = new ShoreCoercingTransformer(
|
||||
withSmoothing,
|
||||
miniMap,
|
||||
);
|
||||
const withMiniMap = new MiniMapTransformer(withShoreCoercing, game, miniMap);
|
||||
|
||||
// Validate that both points are water tiles
|
||||
if (!game.isWater(fromRef)) {
|
||||
throw new Error(`Start point (${from[0]}, ${from[1]}) is not water`);
|
||||
}
|
||||
if (!game.isWater(toRef)) {
|
||||
throw new Error(`End point (${to[0]}, ${to[1]}) is not water`);
|
||||
}
|
||||
|
||||
// Compute NavMesh path
|
||||
const navMeshPath = navMeshAdapter.findPath(fromRef, toRef, true);
|
||||
const path = pathToCoords(navMeshPath, game);
|
||||
return withMiniMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute primary path using AStarWaterHierarchical with debug info
|
||||
* Uses the same transformer chain as PathFinding.Water()
|
||||
*/
|
||||
function computePrimaryPath(
|
||||
hpaStar: AStarWaterHierarchical,
|
||||
game: any,
|
||||
graph: any,
|
||||
fromRef: TileRef,
|
||||
toRef: TileRef,
|
||||
): PrimaryResult {
|
||||
const miniMap = game.miniMap();
|
||||
|
||||
// Extract debug info
|
||||
let gateways: Array<[number, number]> | null = null;
|
||||
// Build wrapped pathfinder with all transformers
|
||||
const wrappedPf = buildWrappedPathFinder(hpaStar, game, graph);
|
||||
|
||||
// Enable debug mode to capture internal state
|
||||
hpaStar.debugMode = true;
|
||||
|
||||
const start = performance.now();
|
||||
const path = wrappedPf.findPath(fromRef, toRef);
|
||||
const time = performance.now() - start;
|
||||
|
||||
const debugInfo = hpaStar.debugInfo;
|
||||
|
||||
// Convert node path (miniMap coords) to full map coords
|
||||
let nodePath: Array<[number, number]> | null = null;
|
||||
if (debugInfo?.nodePath) {
|
||||
nodePath = debugInfo.nodePath.map((tile: TileRef) => {
|
||||
const x = miniMap.x(tile) * 2;
|
||||
const y = miniMap.y(tile) * 2;
|
||||
return [x, y] as [number, number];
|
||||
});
|
||||
}
|
||||
|
||||
// Convert initialPath (miniMap TileRefs) to full map coords
|
||||
let initialPath: Array<[number, number]> | null = null;
|
||||
let timings: any = {};
|
||||
|
||||
if (navMeshAdapter.debugInfo) {
|
||||
// Convert gatewayPath (TileRefs on miniMap) to full map coordinates
|
||||
if (navMeshAdapter.debugInfo.gatewayPath) {
|
||||
gateways = navMeshAdapter.debugInfo.gatewayPath.map((tile: TileRef) => {
|
||||
const x = miniMap.x(tile) * 2;
|
||||
const y = miniMap.y(tile) * 2;
|
||||
return [x, y] as [number, number];
|
||||
});
|
||||
}
|
||||
|
||||
// Convert initial path
|
||||
if (navMeshAdapter.debugInfo.initialPath) {
|
||||
initialPath = navMeshAdapter.debugInfo.initialPath.map(
|
||||
(tile: TileRef) => [game.x(tile), game.y(tile)] as [number, number],
|
||||
);
|
||||
}
|
||||
|
||||
timings = navMeshAdapter.debugInfo.timings || {};
|
||||
if (debugInfo?.initialPath) {
|
||||
initialPath = debugInfo.initialPath.map((tile: TileRef) => {
|
||||
const x = miniMap.x(tile) * 2;
|
||||
const y = miniMap.y(tile) * 2;
|
||||
return [x, y] as [number, number];
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
initialPath,
|
||||
gateways,
|
||||
timings,
|
||||
path: pathToCoords(path, game),
|
||||
length: path ? path.length : 0,
|
||||
time: timings.total ?? 0,
|
||||
time,
|
||||
debug: {
|
||||
nodePath,
|
||||
initialPath,
|
||||
timings: debugInfo?.timings ?? {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute only PathFinder.Mini path
|
||||
* Compute comparison path using adapter
|
||||
*/
|
||||
export async function computePfMiniPath(
|
||||
mapName: string,
|
||||
from: [number, number],
|
||||
to: [number, number],
|
||||
): Promise<PfMiniResult> {
|
||||
const { game } = await loadMap(mapName);
|
||||
|
||||
// Convert coordinates to TileRefs
|
||||
const fromRef = game.ref(from[0], from[1]);
|
||||
const toRef = game.ref(to[0], to[1]);
|
||||
|
||||
// Validate that both points are water tiles
|
||||
if (!game.isWater(fromRef)) {
|
||||
throw new Error(`Start point (${from[0]}, ${from[1]}) is not water`);
|
||||
}
|
||||
if (!game.isWater(toRef)) {
|
||||
throw new Error(`End point (${to[0]}, ${to[1]}) is not water`);
|
||||
}
|
||||
|
||||
// Compute PathFinder.Mini path
|
||||
const pfMiniAdapter = getPfMiniAdapter(mapName, game);
|
||||
const pfMiniStart = performance.now();
|
||||
const pfMiniPath = pfMiniAdapter.findPath(fromRef, toRef);
|
||||
const pfMiniEnd = performance.now();
|
||||
|
||||
const path = pathToCoords(pfMiniPath, game);
|
||||
const time = pfMiniEnd - pfMiniStart;
|
||||
function computeComparisonPath(
|
||||
adapter: SteppingPathFinder<TileRef>,
|
||||
game: any,
|
||||
fromRef: TileRef,
|
||||
toRef: TileRef,
|
||||
adapterName: string,
|
||||
): ComparisonResult {
|
||||
const start = performance.now();
|
||||
const path = adapter.findPath(fromRef, toRef);
|
||||
const time = performance.now() - start;
|
||||
|
||||
return {
|
||||
path,
|
||||
adapter: adapterName,
|
||||
path: pathToCoords(path, game),
|
||||
length: path ? path.length : 0,
|
||||
time,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute pathfinding between two points
|
||||
*/
|
||||
export async function computePath(
|
||||
mapName: string,
|
||||
from: [number, number],
|
||||
to: [number, number],
|
||||
options: { adapters?: string[] } = {},
|
||||
): Promise<PathfindResult> {
|
||||
const { game, hpaStar } = await loadMap(mapName);
|
||||
const graph = game.miniWaterGraph();
|
||||
|
||||
// Convert coordinates to TileRefs
|
||||
const fromRef = game.ref(from[0], from[1]);
|
||||
const toRef = game.ref(to[0], to[1]);
|
||||
|
||||
// Validate that both points are water tiles
|
||||
if (!game.isWater(fromRef)) {
|
||||
throw new Error(`Start point (${from[0]}, ${from[1]}) is not water`);
|
||||
}
|
||||
if (!game.isWater(toRef)) {
|
||||
throw new Error(`End point (${to[0]}, ${to[1]}) is not water`);
|
||||
}
|
||||
|
||||
// Compute primary path (HPA* with debug)
|
||||
const primary = computePrimaryPath(hpaStar, game, graph, fromRef, toRef);
|
||||
|
||||
// Compute comparison paths
|
||||
const selectedAdapters = options.adapters ?? COMPARISON_ADAPTERS;
|
||||
const comparisons: ComparisonResult[] = [];
|
||||
|
||||
for (const adapterName of selectedAdapters) {
|
||||
if (!COMPARISON_ADAPTERS.includes(adapterName)) {
|
||||
console.warn(`Unknown adapter: ${adapterName}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const adapter = getOrCreateAdapter(mapName, adapterName, game);
|
||||
const result = computeComparisonPath(
|
||||
adapter,
|
||||
game,
|
||||
fromRef,
|
||||
toRef,
|
||||
adapterName,
|
||||
);
|
||||
comparisons.push(result);
|
||||
} catch (error) {
|
||||
console.error(`Error with adapter ${adapterName}:`, error);
|
||||
comparisons.push({
|
||||
adapter: adapterName,
|
||||
path: null,
|
||||
length: 0,
|
||||
time: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { primary, comparisons };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear pathfinding adapter caches
|
||||
*/
|
||||
export function clearAdapterCaches() {
|
||||
pfMiniCache.clear();
|
||||
adapterCache.clear();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -129,13 +129,13 @@
|
||||
<button class="toggle-button" id="showInitialPath" data-active="false">
|
||||
Initial Path
|
||||
</button>
|
||||
<button class="toggle-button" id="showUsedGateways" data-active="false">
|
||||
Used Gateways
|
||||
<button class="toggle-button" id="showUsedNodes" data-active="false">
|
||||
Used Nodes
|
||||
</button>
|
||||
</div>
|
||||
<div class="debug-panel-row">
|
||||
<button class="toggle-button" id="showGateways" data-active="false">
|
||||
Gateways
|
||||
<button class="toggle-button" id="showNodes" data-active="false">
|
||||
Nodes
|
||||
</button>
|
||||
<button class="toggle-button" id="showSectorGrid" data-active="false">
|
||||
Sectors
|
||||
@@ -166,75 +166,42 @@
|
||||
<div class="timing-label">
|
||||
<button
|
||||
class="refresh-icon"
|
||||
id="refreshNavMesh"
|
||||
title="Recompute NavMesh path"
|
||||
id="refreshHpa"
|
||||
title="Recompute HPA* path"
|
||||
>
|
||||
<span>↻</span>
|
||||
</button>
|
||||
NavMesh <span class="timing-label-detail" id="navMeshTiles"></span>
|
||||
HPA* <span class="timing-label-detail" id="hpaTiles"></span>
|
||||
</div>
|
||||
<div class="timing-value-large" id="navMeshTime">—</div>
|
||||
<div class="timing-value-large" id="hpaTime">—</div>
|
||||
|
||||
<div class="timing-breakdown" id="timingBreakdown">
|
||||
<div class="timing-item" id="timingEarlyExit" style="display: none">
|
||||
<span class="timing-name">Early Exit:</span>
|
||||
<span class="timing-value" id="timingEarlyExitValue">—</span>
|
||||
</div>
|
||||
<div class="timing-item" id="timingFindNodes" style="display: none">
|
||||
<span class="timing-name">Find Nodes:</span>
|
||||
<span class="timing-value" id="timingFindNodesValue">—</span>
|
||||
</div>
|
||||
<div
|
||||
class="timing-item"
|
||||
id="timingFindGateways"
|
||||
id="timingAbstractPath"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="timing-name">Find Gateways:</span>
|
||||
<span class="timing-value" id="timingFindGatewaysValue">—</span>
|
||||
</div>
|
||||
<div class="timing-item" id="timingGatewayPath" style="display: none">
|
||||
<span class="timing-name">Gateway Path:</span>
|
||||
<span class="timing-value" id="timingGatewayPathValue">—</span>
|
||||
<span class="timing-name">Abstract Path:</span>
|
||||
<span class="timing-value" id="timingAbstractPathValue">—</span>
|
||||
</div>
|
||||
<div class="timing-item" id="timingInitialPath" style="display: none">
|
||||
<span class="timing-name">Initial Path:</span>
|
||||
<span class="timing-value" id="timingInitialPathValue">—</span>
|
||||
</div>
|
||||
<div class="timing-item" id="timingSmoothPath" style="display: none">
|
||||
<span class="timing-name">Smooth Path:</span>
|
||||
<span class="timing-value" id="timingSmoothPathValue">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timing-section" id="pfMiniRequestSection">
|
||||
<button
|
||||
id="requestPfMini"
|
||||
class="timing-button"
|
||||
title="PathFinder.Mini is slow (50-1800ms per path). Click to compare."
|
||||
disabled
|
||||
>
|
||||
Request PathFinder.Mini
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="timing-section"
|
||||
id="pfMiniTimingSection"
|
||||
style="display: none"
|
||||
>
|
||||
<div class="timing-label">
|
||||
<button
|
||||
class="refresh-icon"
|
||||
id="refreshPfMini"
|
||||
title="Recompute PF.Mini path"
|
||||
>
|
||||
<span>↻</span>
|
||||
</button>
|
||||
PF.Mini <span class="timing-label-detail" id="pfMiniTiles"></span>
|
||||
</div>
|
||||
<div class="timing-value-large" id="pfMiniTime">—</div>
|
||||
</div>
|
||||
|
||||
<div class="timing-section" id="speedupSection" style="display: none">
|
||||
<div class="timing-label">Speedup</div>
|
||||
<div class="timing-value-speedup" id="speedupValue">—</div>
|
||||
<div class="timing-section" id="comparisonsSection" style="display: none">
|
||||
<div class="timing-label">Comparisons</div>
|
||||
<div id="comparisonsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -256,13 +223,9 @@
|
||||
></div>
|
||||
<span>End Point</span>
|
||||
</div>
|
||||
<div class="legend-item" id="pfMiniLegend" style="display: none">
|
||||
<div class="legend-color" style="background: #ffaa00"></div>
|
||||
<span>PathFinder.Mini</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #00ffff"></div>
|
||||
<span>NavMesh</span>
|
||||
<span>HPA*</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #ff00ff"></div>
|
||||
@@ -273,7 +236,7 @@
|
||||
class="legend-color"
|
||||
style="background: #ffff00; height: 8px"
|
||||
></div>
|
||||
<span>Used Gateways</span>
|
||||
<span>Used Nodes</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div
|
||||
@@ -285,7 +248,7 @@
|
||||
border-radius: 50%;
|
||||
"
|
||||
></div>
|
||||
<span>Gateways</span>
|
||||
<span>Nodes</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div
|
||||
|
||||
@@ -500,6 +500,67 @@ canvas {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Comparison rows */
|
||||
.comparison-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
margin: 0 -8px;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #333;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.comparison-row:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.comparison-row.active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.comparison-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.comp-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.comparison-row.active .comp-color {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.comp-name {
|
||||
color: #aaa;
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.comp-tiles {
|
||||
font-family: monospace;
|
||||
color: #888;
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.comp-time {
|
||||
font-family: monospace;
|
||||
color: #f5f5f5;
|
||||
width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Legend panel (right side) */
|
||||
.legend-panel {
|
||||
position: fixed;
|
||||
|
||||
@@ -8,11 +8,7 @@ import {
|
||||
listMaps,
|
||||
setConfig,
|
||||
} from "./api/maps.js";
|
||||
import {
|
||||
clearAdapterCaches,
|
||||
computePath,
|
||||
computePfMiniPath,
|
||||
} from "./api/pathfinding.js";
|
||||
import { clearAdapterCaches, computePath } from "./api/pathfinding.js";
|
||||
|
||||
// Parse command-line arguments
|
||||
const args = process.argv.slice(2);
|
||||
@@ -112,12 +108,18 @@ app.get("/api/maps/:name/thumbnail", (req: Request, res: Response) => {
|
||||
* map: string,
|
||||
* from: [x, y],
|
||||
* to: [x, y],
|
||||
* includePfMini?: boolean
|
||||
* adapters?: string[] // Optional: which comparison adapters to run
|
||||
* }
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* primary: { path, length, time, debug: { nodePath, initialPath, timings } },
|
||||
* comparisons: [{ adapter, path, length, time }, ...]
|
||||
* }
|
||||
*/
|
||||
app.post("/api/pathfind", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { map, from, to, includePfMini } = req.body;
|
||||
const { map, from, to, adapters } = req.body;
|
||||
|
||||
// Validate request
|
||||
if (!map || !from || !to) {
|
||||
@@ -144,7 +146,7 @@ app.post("/api/pathfind", async (req: Request, res: Response) => {
|
||||
map,
|
||||
from as [number, number],
|
||||
to as [number, number],
|
||||
{ includePfMini: !!includePfMini },
|
||||
{ adapters },
|
||||
);
|
||||
|
||||
res.json(result);
|
||||
@@ -165,66 +167,6 @@ app.post("/api/pathfind", async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/pathfind-pfmini
|
||||
* Compute only PathFinder.Mini path
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* map: string,
|
||||
* from: [x, y],
|
||||
* to: [x, y]
|
||||
* }
|
||||
*/
|
||||
app.post("/api/pathfind-pfmini", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { map, from, to } = req.body;
|
||||
|
||||
// Validate request
|
||||
if (!map || !from || !to) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid request",
|
||||
message: "Missing required fields: map, from, to",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!Array.isArray(from) ||
|
||||
from.length !== 2 ||
|
||||
!Array.isArray(to) ||
|
||||
to.length !== 2
|
||||
) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid coordinates",
|
||||
message: "from and to must be [x, y] coordinate arrays",
|
||||
});
|
||||
}
|
||||
|
||||
// Compute PF.Mini path only
|
||||
const result = await computePfMiniPath(
|
||||
map,
|
||||
from as [number, number],
|
||||
to as [number, number],
|
||||
);
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error("Error computing PF.Mini path:", error);
|
||||
|
||||
if (error instanceof Error && error.message.includes("is not water")) {
|
||||
res.status(400).json({
|
||||
error: "Invalid coordinates",
|
||||
message: error.message,
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
error: "Failed to compute PF.Mini path",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/cache/clear
|
||||
* Clear all caches (useful for development)
|
||||
|
||||
Reference in New Issue
Block a user