Fix transport ship src and dst to always be water (#2832)

## Description:

Issue discovered by @DevelopingTom, posted on Discord. After merging
pathfinding PR, transport ships lost the ability to navigate between
land tiles. For now, the quick fix is to select adjacent water tile and
select it for pathing. Conquer logic still applies to original
destination on the shore.

## Please complete the following:

- [ ] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] 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:

moleole
This commit is contained in:
Arkadiusz Sygulski
2026-01-10 00:01:44 +01:00
committed by GitHub
parent cf1e67cd9c
commit d8762b1317
2 changed files with 59 additions and 10 deletions
+58 -9
View File
@@ -29,6 +29,7 @@ export class TransportShipExecution implements Execution {
// TODO make private
public path: TileRef[];
private dst: TileRef | null;
private dstShore: TileRef | null;
private boat: Unit;
@@ -103,8 +104,8 @@ export class TransportShipExecution implements Execution {
this.startTroops = Math.min(this.startTroops, this.attacker.troops());
this.dst = targetTransportTile(this.mg, this.ref);
if (this.dst === null) {
this.dstShore = targetTransportTile(this.mg, this.ref);
if (this.dstShore === null) {
console.warn(
`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`,
);
@@ -112,9 +113,18 @@ export class TransportShipExecution implements Execution {
return;
}
this.dst = this.adjacentWater(this.dstShore);
if (this.dst === null) {
console.warn(
`${this.attacker} cannot find water tile adjacent to destination`,
);
this.active = false;
return;
}
const closestTileSrc = this.attacker.canBuild(
UnitType.TransportShip,
this.dst,
this.dstShore,
);
if (closestTileSrc === false) {
console.warn(`can't build transport ship`);
@@ -143,6 +153,22 @@ export class TransportShipExecution implements Execution {
targetTile: this.dst ?? undefined,
});
// Move boat from shore to adjacent water for pathfinding
const spawnWater = this.adjacentWater(this.src);
if (spawnWater === null) {
console.warn(`No adjacent water for transport ship spawn`);
this.boat.delete(false);
this.active = false;
return;
}
this.boat.move(spawnWater);
if (this.dstShore !== null) {
this.boat.setTargetTile(this.dstShore);
} else {
this.boat.setTargetTile(undefined);
}
// Notify the target player about the incoming naval invasion
if (this.targetID && this.targetID !== mg.terraNullius().id()) {
mg.displayIncomingUnit(
@@ -194,6 +220,7 @@ export class TransportShipExecution implements Execution {
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 {
@@ -210,10 +237,19 @@ export class TransportShipExecution implements Execution {
this.active = false;
return;
} else {
this.dst = this.src;
this.dstShore = this.src;
const retreatWater = this.adjacentWater(this.src);
if (retreatWater === null) {
console.warn(`No adjacent water for retreat destination`);
this.attacker.addTroops(this.boat.troops());
this.boat.delete(false);
this.active = false;
return;
}
this.dst = retreatWater;
if (this.boat.targetTile() !== this.dst) {
this.boat.setTargetTile(this.dst);
if (this.boat.targetTile() !== this.dstShore) {
this.boat.setTargetTile(this.dstShore!);
}
}
}
@@ -221,7 +257,7 @@ export class TransportShipExecution implements Execution {
const result = this.pathFinder.next(this.boat.tile(), this.dst);
switch (result.status) {
case PathStatus.COMPLETE:
if (this.mg.owner(this.dst) === this.attacker) {
if (this.mg.owner(this.dstShore!) === this.attacker) {
const deaths = this.boat.troops() * (malusForRetreat / 100);
const survivors = this.boat.troops() - deaths;
this.attacker.addTroops(survivors);
@@ -241,7 +277,7 @@ export class TransportShipExecution implements Execution {
}
return;
}
this.attacker.conquer(this.dst);
this.attacker.conquer(this.dstShore!);
if (this.target.isPlayer() && this.attacker.isFriendly(this.target)) {
this.attacker.addTroops(this.boat.troops());
} else {
@@ -250,7 +286,7 @@ export class TransportShipExecution implements Execution {
this.boat.troops(),
this.attacker,
this.targetID,
this.dst,
this.dstShore!,
false,
),
);
@@ -285,4 +321,17 @@ export class TransportShipExecution implements Execution {
isActive(): boolean {
return this.active;
}
private adjacentWater(tile: TileRef): TileRef | null {
if (this.mg.isWater(tile)) {
return tile;
}
for (const neighbor of this.mg.neighbors(tile)) {
if (this.mg.isWater(neighbor)) {
return neighbor;
}
}
return null;
}
}
+1 -1
View File
@@ -383,7 +383,7 @@ describe("Disconnected", () => {
player1.conquer(game.map().ref(coastX, 4));
player2.conquer(game.map().ref(coastX, 1));
const enemyShoreTile = game.map().ref(coastX, 8);
const enemyShoreTile = game.map().ref(coastX, 15);
game.addExecution(
new TransportShipExecution(