diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index baaff6f51..dd0c8230d 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -138,10 +138,7 @@ export class TradeShipExecution implements Execution { if (dst !== this.motionPlanDst) { this.motionPlanId++; const from = result.node; - const path = this.pathFinder.findPath(from, dst) ?? [from]; - if (path.length === 0 || path[0] !== from) { - path.unshift(from); - } + const path = this.pathFinder.pathForTraversal(from, dst); this.mg.recordMotionPlan({ kind: "grid", diff --git a/src/core/pathfinding/PathFinder.ts b/src/core/pathfinding/PathFinder.ts index 96e117435..037579144 100644 --- a/src/core/pathfinding/PathFinder.ts +++ b/src/core/pathfinding/PathFinder.ts @@ -127,7 +127,7 @@ export class PathFinding { * waterGraphVersion to stagger when each ship invalidates its cached path. */ export class WaterPathFinder implements SteppingPathFinder { - private stepper: SteppingPathFinder; + private stepper: PathFinderStepper; private _waterGraphVersion: number; private _rebuilt = false; @@ -195,11 +195,25 @@ export class WaterPathFinder implements SteppingPathFinder { return this.stepper.next(from, to, dist); } + /** Runs a one-shot query without changing the path consumed by next(). */ findPath(from: TileRef | TileRef[], to: TileRef): TileRef[] | null { this.ensureFresh(); return this.stepper.findPath(from, to); } + /** + * Returns the route following a successful next() call, starting at `from`. + * If refreshing the water graph replaced the stepper, fall back to the same + * one-shot query used before traversal paths were reusable. + */ + pathForTraversal(from: TileRef, to: TileRef): TileRef[] | Uint32Array { + this.ensureFresh(); + const path = + this.stepper.pathAfterNext() ?? this.stepper.findPath(from, to); + if (path === null || path.length === 0) return [from]; + return path[0] === from ? path : [from, ...path]; + } + invalidate(): void { this.stepper.invalidate(); } diff --git a/src/core/pathfinding/PathFinderStepper.ts b/src/core/pathfinding/PathFinderStepper.ts index 52407678b..1efed4cf7 100644 --- a/src/core/pathfinding/PathFinderStepper.ts +++ b/src/core/pathfinding/PathFinderStepper.ts @@ -108,6 +108,16 @@ export class PathFinderStepper implements SteppingPathFinder { this.lastTo = null; } + /** + * Returns a copy of the active path beginning at the node most recently + * returned by next(). Returns null when there is no active traversal. + */ + pathAfterNext(): T[] | Uint32Array | null { + if (this.path === null || this.pathIndex === 0) return null; + return this.path.slice(this.pathIndex - 1); + } + + /** Computes a one-shot route without changing the cached route. */ findPath(from: T | T[], to: T): T[] | null { if (this.config.preCheck) { const fromArray = Array.isArray(from) ? from : [from]; diff --git a/tests/core/executions/TradeShipExecution.test.ts b/tests/core/executions/TradeShipExecution.test.ts index 36b9860cc..650bc80ad 100644 --- a/tests/core/executions/TradeShipExecution.test.ts +++ b/tests/core/executions/TradeShipExecution.test.ts @@ -107,13 +107,17 @@ describe("TradeShipExecution", () => { tradeShipExecution["pathFinder"] = { next: vi.fn(() => ({ status: PathStatus.NEXT, node: 32 })), findPath: vi.fn((from: number) => [from]), + pathForTraversal: vi.fn(() => [32]), } as any; tradeShipExecution["tradeShip"] = tradeShip; }); it("should initialize and tick without errors", () => { + const pathFinder = tradeShipExecution["pathFinder"]; tradeShipExecution.tick(1); expect(tradeShipExecution.isActive()).toBe(true); + expect(pathFinder.pathForTraversal).toHaveBeenCalledOnce(); + expect(pathFinder.findPath).not.toHaveBeenCalled(); }); it("should deactivate if tradeShip is not active", () => { @@ -159,6 +163,7 @@ describe("TradeShipExecution", () => { tradeShipExecution["pathFinder"] = { next: vi.fn(() => ({ status: PathStatus.COMPLETE, node: 32 })), findPath: vi.fn((from: number) => [from]), + pathForTraversal: vi.fn(() => [32]), } as any; tradeShipExecution.tick(1); expect(tradeShip.delete).toHaveBeenCalledWith(false); diff --git a/tests/core/pathfinding/PathFinderStepper.test.ts b/tests/core/pathfinding/PathFinderStepper.test.ts index 5cf0fbc53..6f5febb78 100644 --- a/tests/core/pathfinding/PathFinderStepper.test.ts +++ b/tests/core/pathfinding/PathFinderStepper.test.ts @@ -125,6 +125,22 @@ describe("PathFinderStepper", () => { }); }); + describe("pathAfterNext", () => { + it("returns a copy of the cached remainder", () => { + const pathMap = new Map([["1->5", [1, 2, 3, 4, 5]]]); + const stepper = new PathFinderStepper(createMockFinder(pathMap)); + + expect(stepper.pathAfterNext()).toBeNull(); + expect(stepper.next(1, 5)).toEqual({ status: PathStatus.NEXT, node: 2 }); + const path = stepper.pathAfterNext(); + expect(path).toBeInstanceOf(Uint32Array); + expect(Array.from(path!)).toEqual([2, 3, 4, 5]); + + path![0] = 99; + expect(stepper.next(2, 5)).toEqual({ status: PathStatus.NEXT, node: 3 }); + }); + }); + describe("findPath", () => { it("delegates to inner finder", () => { const pathMap = new Map([["1->5", [1, 2, 3, 4, 5]]]);