Feat: Troop transport retreats to closest owned tile v2 (#3286)

If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #1139

## Description:

New version of the #2789 PR that is cleaner after changes made to old
pathfinding logic.

Adds logic to troop transport retreat behaviour which retreats a
transport to the closest owned tile instead of the source. Now if no
shores are detected (you lost all your shoreline while the transport was
out) we handle the return case same as if the original source was no
longer your territory.

<img width="2541" height="1593" alt="image"
src="https://github.com/user-attachments/assets/4d2ff5e7-d10d-40f4-80e0-9f029cff61a2"
/>

## Video example from previous PR (works the exact same way in this PR):


https://github.com/user-attachments/assets/e43a3b10-e8b0-4f23-87f3-2dc4739de880

## 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:

bijx
This commit is contained in:
bijx
2026-02-24 22:31:06 -05:00
committed by GitHub
parent e39140733b
commit 7855e1b0e9
2 changed files with 15 additions and 14 deletions
+8 -13
View File
@@ -29,6 +29,7 @@ export class TransportShipExecution implements Execution {
private dst: TileRef | null;
private src: TileRef | null;
private retreatDst: TileRef | false | null = null;
private boat: Unit;
private originalOwner: Player;
@@ -156,27 +157,21 @@ export class TransportShipExecution implements Execution {
}
if (this.boat.retreating()) {
// Ensure retreat source is still valid for (new) owner
if (this.mg.owner(this.src!) !== this.attacker) {
// Use bestTransportShipSpawn, not canBuild because of its max boats check etc
const newSrc = this.attacker.bestTransportShipSpawn(this.dst);
if (newSrc === false) {
this.src = null;
} else {
this.src = newSrc;
}
}
// Resolve retreat destination once, based on current boat location when retreat begins.
this.retreatDst ??= this.attacker.bestTransportShipSpawn(
this.boat.tile(),
);
if (this.src === null) {
if (this.retreatDst === false) {
console.warn(
`TransportShipExecution: retreating but no src found for new attacker`,
`TransportShipExecution: retreating but no retreat destination found`,
);
this.attacker.addTroops(this.boat.troops());
this.boat.delete(false);
this.active = false;
return;
} else {
this.dst = this.src;
this.dst = this.retreatDst;
if (this.boat.targetTile() !== this.dst) {
this.boat.setTargetTile(this.dst);
+7 -1
View File
@@ -373,7 +373,7 @@ describe("Disconnected", () => {
expect(game.owner(enemyShoreTile)).toBe(player1);
});
test("Captured transport ship should retreat to owner's shore tile", () => {
test("Captured transport ship should retreat to closest owner shore tile", () => {
player1.conquer(game.map().ref(coastX, 4));
player2.conquer(game.map().ref(coastX, 1));
@@ -397,9 +397,15 @@ describe("Disconnected", () => {
expect(player2.isAlive()).toBe(false);
expect(transportShip.owner()).toBe(player1);
const expectedRetreatTile = player1.bestTransportShipSpawn(
transportShip.tile(),
);
expect(expectedRetreatTile).not.toBe(false);
transportShip.orderBoatRetreat();
executeTicks(game, 2);
expect(transportShip.targetTile()).toBe(expectedRetreatTile);
expect(transportShip.targetTile()).not.toBe(enemyShoreTile);
expect(game.owner(transportShip.targetTile()!)).toBe(player1);
});