mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
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:
@@ -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());
|
||||
|
||||
@@ -852,4 +852,71 @@ describe("Warship", () => {
|
||||
game.executeNextTick();
|
||||
expect(warship.tile()).not.toBe(tileBeforeCombat);
|
||||
});
|
||||
|
||||
test("Warship captures trade ship immediately when already within capture range", async () => {
|
||||
// Trade ship is within Manhattan distance 5 — should be captured on the first tick
|
||||
// via the dist <= 5 fast path in huntDownTradeShip, without needing pathfinding.
|
||||
player1.buildUnit(UnitType.Port, game.ref(coastX, 8), {});
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 8),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 1, 8),
|
||||
},
|
||||
);
|
||||
const tradeShip = player2.buildUnit(
|
||||
UnitType.TradeShip,
|
||||
game.ref(coastX + 1, 11), // Manhattan distance 3 from warship
|
||||
{
|
||||
targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 11), {}),
|
||||
},
|
||||
);
|
||||
|
||||
const execution = new WarshipExecution(warship);
|
||||
const executionInternals = execution as unknown as {
|
||||
findTargetUnit: () => typeof tradeShip | undefined;
|
||||
};
|
||||
execution.init(game, game.ticks());
|
||||
vi.spyOn(executionInternals, "findTargetUnit").mockReturnValue(tradeShip);
|
||||
|
||||
expect(tradeShip.owner().id()).toBe(player2.id());
|
||||
execution.tick(game.ticks());
|
||||
expect(tradeShip.owner()).toBe(player1);
|
||||
});
|
||||
|
||||
test("Warship uses greedy pursuit to capture trade ship within 20 tiles", async () => {
|
||||
// Trade ship is within the 20-tile greedy range but outside the 5-tile instant-capture
|
||||
// range. The warship should use direct neighbor movement (not minimap pathfinding)
|
||||
// and close the gap cleanly.
|
||||
player1.buildUnit(UnitType.Port, game.ref(coastX, 3), {});
|
||||
const warship = player1.buildUnit(
|
||||
UnitType.Warship,
|
||||
game.ref(coastX + 1, 3),
|
||||
{
|
||||
patrolTile: game.ref(coastX + 1, 3),
|
||||
},
|
||||
);
|
||||
const tradeShip = player2.buildUnit(
|
||||
UnitType.TradeShip,
|
||||
game.ref(coastX + 1, 13), // Manhattan distance 10 — within greedy range
|
||||
{
|
||||
targetUnit: player2.buildUnit(UnitType.Port, game.ref(coastX, 13), {}),
|
||||
},
|
||||
);
|
||||
|
||||
const execution = new WarshipExecution(warship);
|
||||
const executionInternals = execution as unknown as {
|
||||
findTargetUnit: () => typeof tradeShip | undefined;
|
||||
};
|
||||
execution.init(game, game.ticks());
|
||||
vi.spyOn(executionInternals, "findTargetUnit").mockReturnValue(tradeShip);
|
||||
|
||||
expect(tradeShip.owner().id()).toBe(player2.id());
|
||||
// 10 tiles at 2 steps/tick = 5 ticks minimum
|
||||
for (let i = 0; i < 10; i++) {
|
||||
execution.tick(game.ticks());
|
||||
if (tradeShip.owner() === player1) break;
|
||||
}
|
||||
expect(tradeShip.owner()).toBe(player1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user