mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 04:53:26 +00:00
Implemented post-refinement “string pulling” and exposed a sparse polyline for later spline-render.
- Added rubberBandWaterPath() to PathRubberBand.ts (bounded LOS checks; returns { path, waypoints }).
- Hooked it into findWaterPathFromSeedsCoarseToFine() so every returned fine water path is now rubber-banded and also carries waypoints in CoarseToFineWaterPath.ts.
- Extended MultiSourceAnyTargetBFSResult with optional waypoints?: TileRef[] in MultiSourceAnyTargetBFS.ts.
- Plumbed full-route waypoints ([src, ...waterWaypoints, dst]) into bestTransportShipRoute() return value in TransportShipUtils.ts (line 318).
This commit is contained in:
@@ -7,6 +7,7 @@ type BoatRoute = {
|
||||
src: TileRef;
|
||||
dst: TileRef;
|
||||
path: TileRef[];
|
||||
waypoints?: TileRef[];
|
||||
};
|
||||
|
||||
function miniMapOrNull(gm: GameMap): GameMap | null {
|
||||
@@ -426,13 +427,15 @@ export function bestTransportShipRoute(
|
||||
const src = result.source;
|
||||
// Full route includes the shore endpoints to drive unit movement.
|
||||
const path = [src, ...result.path, dst];
|
||||
const waterWaypoints = result.waypoints ?? result.path;
|
||||
const waypoints = [src, ...waterWaypoints, dst];
|
||||
console.log(
|
||||
`bestTransportShipRoute: ${duration.toFixed(2)}ms, steps=${Math.max(
|
||||
0,
|
||||
path.length - 1,
|
||||
)}`,
|
||||
);
|
||||
return { src, dst, path };
|
||||
return { src, dst, path, waypoints };
|
||||
}
|
||||
|
||||
export function canBuildTransportShip(
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
MultiSourceAnyTargetBFSOptions,
|
||||
MultiSourceAnyTargetBFSResult,
|
||||
} from "./MultiSourceAnyTargetBFS";
|
||||
import { rubberBandCoarsePath } from "./PathRubberBand";
|
||||
import { rubberBandCoarsePath, rubberBandWaterPath } from "./PathRubberBand";
|
||||
|
||||
export type CoarseToFineWaterPathOptions = {
|
||||
/**
|
||||
@@ -192,24 +192,36 @@ export function findWaterPathFromSeedsCoarseToFine(
|
||||
): MultiSourceAnyTargetBFSResult | null {
|
||||
const fineBfs = getBfs(fineMap);
|
||||
|
||||
const postprocessFine = (
|
||||
result: MultiSourceAnyTargetBFSResult | null,
|
||||
): MultiSourceAnyTargetBFSResult | null => {
|
||||
if (result === null) return null;
|
||||
const rb = rubberBandWaterPath(fineMap, result.path, bfsOpts);
|
||||
return { ...result, path: rb.path, waypoints: rb.waypoints };
|
||||
};
|
||||
|
||||
if (!coarseMap) {
|
||||
return fineBfs.findWaterPathFromSeeds(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
bfsOpts,
|
||||
return postprocessFine(
|
||||
fineBfs.findWaterPathFromSeeds(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
bfsOpts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const mapping = getFineToCoarseMapping(fineMap, coarseMap);
|
||||
if (mapping === null) {
|
||||
return fineBfs.findWaterPathFromSeeds(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
bfsOpts,
|
||||
return postprocessFine(
|
||||
fineBfs.findWaterPathFromSeeds(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
bfsOpts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -242,12 +254,14 @@ export function findWaterPathFromSeedsCoarseToFine(
|
||||
);
|
||||
|
||||
if (coarseSeeds.length === 0 || coarseTargets.length === 0) {
|
||||
return fineBfs.findWaterPathFromSeeds(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
bfsOpts,
|
||||
return postprocessFine(
|
||||
fineBfs.findWaterPathFromSeeds(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
bfsOpts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -262,12 +276,14 @@ export function findWaterPathFromSeedsCoarseToFine(
|
||||
|
||||
if (coarseResult === null) {
|
||||
// Safe fallback: if the coarse map is conservative, we might still have a fine path.
|
||||
return fineBfs.findWaterPathFromSeeds(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
bfsOpts,
|
||||
return postprocessFine(
|
||||
fineBfs.findWaterPathFromSeeds(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
bfsOpts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,14 +352,16 @@ export function findWaterPathFromSeedsCoarseToFine(
|
||||
return newCount;
|
||||
},
|
||||
);
|
||||
if (refined !== null) return refined;
|
||||
if (refined !== null) return postprocessFine(refined);
|
||||
|
||||
// Final fallback: unrestricted fine BFS.
|
||||
return fineBfs.findWaterPathFromSeeds(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
bfsOpts,
|
||||
return postprocessFine(
|
||||
fineBfs.findWaterPathFromSeeds(
|
||||
fineMap,
|
||||
seedNodes,
|
||||
seedOrigins,
|
||||
targets,
|
||||
bfsOpts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ export type MultiSourceAnyTargetBFSResult = {
|
||||
source: TileRef;
|
||||
target: TileRef;
|
||||
path: TileRef[];
|
||||
/**
|
||||
* Optional sparse polyline representation of `path` (e.g. for rendering/splines).
|
||||
* When present, `path` should still be treated as the authoritative tile-valid route.
|
||||
*/
|
||||
waypoints?: TileRef[];
|
||||
};
|
||||
|
||||
export type MultiSourceAnyTargetBFSOptions = {
|
||||
|
||||
@@ -2,6 +2,11 @@ import { GameMap, TileRef } from "../game/GameMap";
|
||||
import { BezenhamLine } from "../utilities/Line";
|
||||
import { MultiSourceAnyTargetBFSOptions } from "./MultiSourceAnyTargetBFS";
|
||||
|
||||
export type RubberBandPathResult = {
|
||||
waypoints: TileRef[];
|
||||
path: TileRef[];
|
||||
};
|
||||
|
||||
function sign(n: number): -1 | 0 | 1 {
|
||||
return n === 0 ? 0 : n > 0 ? 1 : -1;
|
||||
}
|
||||
@@ -12,6 +17,7 @@ function lineOfSightWater(
|
||||
to: TileRef,
|
||||
noCornerCutting: boolean,
|
||||
): boolean {
|
||||
const w = gm.width();
|
||||
const x0 = gm.x(from);
|
||||
const y0 = gm.y(from);
|
||||
const x1 = gm.x(to);
|
||||
@@ -23,15 +29,15 @@ function lineOfSightWater(
|
||||
let prevY = y0;
|
||||
let point = line.increment();
|
||||
while (point !== true) {
|
||||
const t = gm.ref(point.x, point.y);
|
||||
const t = point.y * w + point.x;
|
||||
if (!gm.isWater(t)) return false;
|
||||
|
||||
if (noCornerCutting) {
|
||||
const dx = sign(point.x - prevX);
|
||||
const dy = sign(point.y - prevY);
|
||||
if (dx !== 0 && dy !== 0) {
|
||||
const orthoA = gm.ref(prevX + dx, prevY);
|
||||
const orthoB = gm.ref(prevX, prevY + dy);
|
||||
const orthoA = prevY * w + (prevX + dx);
|
||||
const orthoB = (prevY + dy) * w + prevX;
|
||||
if (!gm.isWater(orthoA) || !gm.isWater(orthoB)) return false;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +51,7 @@ function lineOfSightWater(
|
||||
}
|
||||
|
||||
function expandLine(gm: GameMap, from: TileRef, to: TileRef, out: TileRef[]) {
|
||||
const w = gm.width();
|
||||
const x0 = gm.x(from);
|
||||
const y0 = gm.y(from);
|
||||
const x1 = gm.x(to);
|
||||
@@ -52,7 +59,7 @@ function expandLine(gm: GameMap, from: TileRef, to: TileRef, out: TileRef[]) {
|
||||
const line = new BezenhamLine({ x: x0, y: y0 }, { x: x1, y: y1 });
|
||||
let point = line.increment();
|
||||
while (point !== true) {
|
||||
const t = gm.ref(point.x, point.y);
|
||||
const t = point.y * w + point.x;
|
||||
if (out.length === 0 || out[out.length - 1] !== t) out.push(t);
|
||||
point = line.increment();
|
||||
}
|
||||
@@ -103,3 +110,51 @@ export function rubberBandCoarsePath(
|
||||
}
|
||||
return spine.length > 0 ? spine : [...coarsePath];
|
||||
}
|
||||
|
||||
/**
|
||||
* "String pulling" / rubber banding for a water-only tile path.
|
||||
*
|
||||
* Returns both:
|
||||
* - `waypoints`: a sparse polyline (for rendering/splines later)
|
||||
* - `path`: a tile-valid path expanded along the waypoint segments
|
||||
*
|
||||
* This is bounded (lookahead + checks per anchor) to keep it hot-path friendly.
|
||||
*/
|
||||
export function rubberBandWaterPath(
|
||||
gm: GameMap,
|
||||
waterPath: readonly TileRef[],
|
||||
bfsOpts: MultiSourceAnyTargetBFSOptions,
|
||||
): RubberBandPathResult {
|
||||
if (waterPath.length <= 2) return { waypoints: [...waterPath], path: [...waterPath] };
|
||||
|
||||
const kingMoves = bfsOpts.kingMoves ?? true;
|
||||
if (!kingMoves) return { waypoints: [...waterPath], path: [...waterPath] };
|
||||
const noCornerCutting = bfsOpts.noCornerCutting ?? true;
|
||||
|
||||
const maxLookahead = 2048;
|
||||
const maxChecksPerAnchor = 96;
|
||||
|
||||
const waypoints: TileRef[] = [waterPath[0]!];
|
||||
let i = 0;
|
||||
while (i < waterPath.length - 1) {
|
||||
const end = Math.min(waterPath.length - 1, i + maxLookahead);
|
||||
let best = i + 1;
|
||||
let checks = 0;
|
||||
for (let j = end; j > i; j--) {
|
||||
if (lineOfSightWater(gm, waterPath[i]!, waterPath[j]!, noCornerCutting)) {
|
||||
best = j;
|
||||
break;
|
||||
}
|
||||
if (++checks >= maxChecksPerAnchor) break;
|
||||
}
|
||||
waypoints.push(waterPath[best]!);
|
||||
i = best;
|
||||
}
|
||||
|
||||
const out: TileRef[] = [];
|
||||
for (let k = 0; k < waypoints.length - 1; k++) {
|
||||
expandLine(gm, waypoints[k]!, waypoints[k + 1]!, out);
|
||||
}
|
||||
|
||||
return out.length > 0 ? { waypoints, path: out } : { waypoints: [...waterPath], path: [...waterPath] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user