Fix warship diagonal chase and improve trade ship capture reliability (#3807)

## Description:

The warship pathfinder operates on a 2x downscaled mini-map, and
upscaling mini-map paths back to full coordinates produces diagonal
interpolated steps. At close range (< 20 tiles), the entire path
consists of these diagonal moves, causing the warship to approach the
trade ship at an awkward angle and never converge cleanly.
## 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:

evan
This commit is contained in:
Evan
2026-04-30 16:37:44 -06:00
committed by GitHub
parent 4f20d2b332
commit ccb80f4245
2 changed files with 107 additions and 10 deletions
+40 -10
View File
@@ -634,29 +634,59 @@ export class WarshipExecution implements Execution {
private huntDownTradeShip() {
this.warship.updateWarshipState({ isInCombat: true });
for (let i = 0; i < 2; i++) {
// target is trade ship so capture it.
const result = this.pathfinder.next(
this.warship.tile(),
this.warship.targetUnit()!.tile(),
5,
);
const target = this.warship.targetUnit()!;
const targetTile = target.tile();
const dist = this.mg.manhattanDist(this.warship.tile(), targetTile);
if (dist <= 5) {
this.warship.owner().captureUnit(target);
this.warship.setTargetUnit(undefined);
this.warship.touch();
return;
}
// When close, the minimap (2x scale) produces diagonal upscaled paths that
// make it hard to converge. Use direct greedy movement instead.
if (dist <= 20) {
const nextTile = this.bestNeighborToward(targetTile);
if (nextTile !== undefined) {
this.warship.move(nextTile);
continue;
}
}
const result = this.pathfinder.next(this.warship.tile(), targetTile, 5);
switch (result.status) {
case PathStatus.COMPLETE:
this.warship.owner().captureUnit(this.warship.targetUnit()!);
this.warship.owner().captureUnit(target);
this.warship.setTargetUnit(undefined);
this.warship.move(this.warship.tile());
this.warship.touch();
return;
case PathStatus.NEXT:
this.warship.move(result.node);
break;
case PathStatus.NOT_FOUND: {
case PathStatus.NOT_FOUND:
console.log(`path not found to target`);
break;
}
}
}
}
private bestNeighborToward(targetTile: TileRef): TileRef | undefined {
const warshipTile = this.warship.tile();
let best: TileRef | undefined;
let bestDist = this.mg.manhattanDist(warshipTile, targetTile);
this.mg.forEachNeighbor(warshipTile, (neighbor) => {
if (!this.mg.isWater(neighbor)) return;
const d = this.mg.manhattanDist(neighbor, targetTile);
if (d < bestDist) {
bestDist = d;
best = neighbor;
}
});
return best;
}
private patrol() {
if (this.warship.targetTile() === undefined) {
this.warship.setTargetTile(this.randomTile());