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:
scamiv
2025-12-28 23:17:05 +01:00
parent b1f05abae0
commit ae9a8cc87d
4 changed files with 118 additions and 37 deletions
+4 -1
View File
@@ -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(
+50 -32
View File
@@ -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 = {
+59 -4
View File
@@ -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] };
}