mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 14:43:25 +00:00
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:
@@ -83,6 +83,10 @@ export class WarshipExecution implements Execution {
|
||||
const patrolTile = this.warship.patrolTile()!;
|
||||
const patrolRangeSquared = config.warshipPatrolRange() ** 2;
|
||||
|
||||
// Lazy: only computed if a TradeShip candidate forces the component check.
|
||||
// `undefined` = not yet computed; `null` = computed, no component found.
|
||||
let warshipComponent: number | null | undefined = undefined;
|
||||
|
||||
const ships = mg.nearbyUnits(
|
||||
this.warship.tile()!,
|
||||
config.warshipTargettingRange(),
|
||||
@@ -113,6 +117,17 @@ export class WarshipExecution implements Execution {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (warshipComponent === undefined) {
|
||||
warshipComponent = mg.getWaterComponent(this.warship.tile());
|
||||
}
|
||||
if (
|
||||
warshipComponent !== null &&
|
||||
!mg.hasWaterComponent(unit.tile(), warshipComponent)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
mg.euclideanDistSquared(patrolTile, unit.tile()) > patrolRangeSquared
|
||||
) {
|
||||
@@ -220,6 +235,7 @@ export class WarshipExecution implements Execution {
|
||||
break;
|
||||
case PathStatus.NOT_FOUND: {
|
||||
console.log(`path not found to target`);
|
||||
this.warship.setTargetTile(undefined);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user