Pathfinding Fixes (Water Nukes / Lakes) 💧 (#3714)

## Description:

Fixes water-pathfinding errors that started appearing after the first
water nuke and persisted across the rest of the match.
Users reported warships "getting stuck" (stopped moving).

<img width="374" height="281" alt="image"
src="https://github.com/user-attachments/assets/de38b8f1-c4d8-469e-b3a7-d0cef4dfb772"
/>

### Summary

- The new `AbstractGraphBuilder.buildClusterConnectionsFromCache` was
buggy _(The cached edge costs reused by "clean" clusters were keyed by
tile pair without their original `(clusterX, clusterY)`, so a boundary
edge could be re-stamped with the wrong cluster and become untraversable
by the query-time single-cluster bounded A*. The cache now stores `{
cost, clusterX, clusterY }` and `buildClusterConnectionsFromCache`
preserves the original attribution when re-adding the edge.)_
- Warships: `findTargetUnit` now skips trade ships that are not in the
warship's water component, avoiding pathfinding to provably unreachable
targets.
- Warships: On `patrol` `NOT_FOUND`, clear `targetTile` so the warship
picks a new target. This is a defensive guard for the rare case where a
water nuke splits the component between target selection and pathfinding
- without it, the warship retries the same now-unreachable target every
tick and spams the log forever.

### Test

- Added a Warship test verifying that trade ships in a different water
component are not targeted.

## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

FloPinguin
This commit is contained in:
FloPinguin
2026-04-19 06:32:21 +02:00
committed by GitHub
parent 4e04bed44c
commit 12b06fa0b2
3 changed files with 72 additions and 6 deletions
@@ -219,7 +219,10 @@ export class AbstractGraphBuilder {
// Partial rebuild state
private cleanClusters: Set<number> | null = null;
private oldEdgeCosts: Map<number, Map<number, number>> | null = null;
private oldEdgeCosts: Map<
number,
Map<number, { cost: number; clusterX: number; clusterY: number }>
> | null = null;
constructor(
private readonly map: GameMap,
@@ -664,8 +667,12 @@ export class AbstractGraphBuilder {
this.oldEdgeCosts.set(tileMin, inner);
}
const existing = inner.get(tileMax);
if (existing === undefined || edge.cost < existing) {
inner.set(tileMax, edge.cost);
if (existing === undefined || edge.cost < existing.cost) {
inner.set(tileMax, {
cost: edge.cost,
clusterX: edge.clusterX,
clusterY: edge.clusterY,
});
}
}
}
@@ -690,9 +697,19 @@ export class AbstractGraphBuilder {
const tileMin = Math.min(nodes[i].tile, nodes[j].tile);
const tileMax = Math.max(nodes[i].tile, nodes[j].tile);
const cost = oldEdgeCosts.get(tileMin)?.get(tileMax);
if (cost !== undefined) {
this.addOrUpdateEdge(nodes[i].id, nodes[j].id, cost, cx, cy);
const entry = oldEdgeCosts.get(tileMin)?.get(tileMax);
if (entry !== undefined) {
// Preserve the ORIGINAL (clusterX, clusterY) from the old graph.
// The path for a boundary edge between two clusters lives in whichever
// cluster's BFS originally found it; attributing it to `cx,cy` here
// would break query-time single-cluster bounded A*.
this.addOrUpdateEdge(
nodes[i].id,
nodes[j].id,
entry.cost,
entry.clusterX,
entry.clusterY,
);
}
}
}