From d8762b131782c5fc1d515aa02f80fdadd0ffb8dd Mon Sep 17 00:00:00 2001 From: Arkadiusz Sygulski Date: Sat, 10 Jan 2026 00:01:44 +0100 Subject: [PATCH] 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 --- src/core/execution/TransportShipExecution.ts | 67 +++++++++++++++++--- tests/Disconnected.test.ts | 2 +- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index afa6c9c39..776644aa2 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -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; + } } diff --git a/tests/Disconnected.test.ts b/tests/Disconnected.test.ts index ca3cfc45b..693cbdf86 100644 --- a/tests/Disconnected.test.ts +++ b/tests/Disconnected.test.ts @@ -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(