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-18 21:32:21 -07:00
committed by GitHub
parent 4e04bed44c
commit 12b06fa0b2
3 changed files with 72 additions and 6 deletions
+33
View File
@@ -7,6 +7,7 @@ import {
PlayerType,
UnitType,
} from "../src/core/game/Game";
import { TileRef } from "../src/core/game/GameMap";
import { setup } from "./util/Setup";
import { executeTicks } from "./util/utils";
@@ -200,6 +201,38 @@ describe("Warship", () => {
expect(tradeShip.owner().id()).toBe(player2.id());
});
test("Warship does not target trade ships in different water components", async () => {
// build port so warship can target trade ships
player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
const warshipTile = game.ref(coastX + 1, 2);
const tradeShipTile = game.ref(coastX + 1, 12);
const warship = player1.buildUnit(UnitType.Warship, warshipTile, {
patrolTile: warshipTile,
});
game.addExecution(new WarshipExecution(warship));
const tradeShip = player2.buildUnit(UnitType.TradeShip, tradeShipTile, {
targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 10), {}),
});
// Mock different water components
game.getWaterComponent = (tile: TileRef) => {
if (tile === warshipTile) return 1;
return 2;
};
game.hasWaterComponent = (tile: TileRef, component: number) => {
return game.getWaterComponent(tile) === component;
};
executeTicks(game, 10);
// Trade ship should not be captured because it's in a different component
expect(tradeShip.owner().id()).toBe(player2.id());
});
test("MoveWarshipExecution fails if player is not the owner", async () => {
const originalPatrolTile = game.ref(coastX + 1, 10);
const warship = player1.buildUnit(