From 6a6c1423888618c3b5a016b0ac2a1b72f5df2b8a Mon Sep 17 00:00:00 2001 From: TKTK123456 <103334266+TKTK123456@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:31:21 -0400 Subject: [PATCH] Fixed pirating not being disabled when a warship's owner has no port in that body of water (#4458) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > **Before opening a PR:** discuss new features on [Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small improvements as [issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose). You must be assigned to an `approved` issue — unsolicited PRs will be auto-closed. **Add approved & assigned issue number here:** Resolves #4291 ## Description: Fixes pirating not being disabled when a warship's owner has no port in that body of water ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: tktk1234567 --- src/core/execution/WarshipExecution.ts | 28 +++++++++--------- tests/Warship.test.ts | 39 +++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index f758a06e2..377ad789c 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -248,7 +248,7 @@ export class WarshipExecution implements Execution { ); // Trade-ship-specific state, lazily computed. - let hasPort: boolean | undefined; + let hasReachablePort: boolean | undefined; let patrolTile: number | undefined; let patrolRangeSquared: number | undefined; let warshipComponent: number | null | undefined = undefined; @@ -272,13 +272,24 @@ export class WarshipExecution implements Execution { const type = unit.type(); if (includeTradeShips && type === UnitType.TradeShip) { - if (hasPort === undefined) { - hasPort = owner.unitCount(UnitType.Port) > 0; + if (warshipComponent === undefined) { + warshipComponent = mg.getWaterComponent(this.warship.tile()); + hasReachablePort = + warshipComponent !== null && + owner + .units(UnitType.Port) + .some( + (port) => + port.isActive() && + !port.isMarkedForDeletion() && + !port.isUnderConstruction() && + mg.hasWaterComponent(port.tile(), warshipComponent!), + ); patrolTile = this.warship.warshipState().patrolTile; patrolRangeSquared = config.warshipPatrolRange() ** 2; } if ( - !hasPort || + !hasReachablePort || patrolTile === undefined || unit.isSafeFromPirates() || unit.targetUnit()?.owner() === owner || @@ -286,15 +297,6 @@ export class WarshipExecution implements Execution { ) { continue; } - if (warshipComponent === undefined) { - warshipComponent = mg.getWaterComponent(this.warship.tile()); - } - if ( - warshipComponent !== null && - !mg.hasWaterComponent(unit.tile(), warshipComponent) - ) { - continue; - } if ( mg.euclideanDistSquared(patrolTile, unit.tile()) > patrolRangeSquared! ) { diff --git a/tests/Warship.test.ts b/tests/Warship.test.ts index b42aea41a..ce4338699 100644 --- a/tests/Warship.test.ts +++ b/tests/Warship.test.ts @@ -34,7 +34,9 @@ describe("Warship", () => { // Advance past the manualMoveRetreatDisabledDuration window. executeTicks(game, 50); }); - + afterEach(() => { + vi.restoreAllMocks(); + }); test("Warship heals only if player has port", async () => { const maxHealth = game.config().unitInfo(UnitType.Warship).maxHealth; if (typeof maxHealth !== "number") { @@ -158,6 +160,41 @@ describe("Warship", () => { expect(tradeShip.owner().id()).toBe(player2.id()); }); + test("Warship doesn't capture trade if there is no port on it's water component", async () => { + const portTile = game.ref(coastX, 10); + player1.buildUnit(UnitType.Port, portTile, {}); + const warship = player1.buildUnit( + UnitType.Warship, + game.ref(coastX + 1, 11), + { + patrolTile: game.ref(coastX + 1, 11), + }, + ); + game.addExecution(new WarshipExecution(warship)); + + const tradeShip = player2.buildUnit( + UnitType.TradeShip, + game.ref(coastX + 1, 11), + { + targetUnit: player1.buildUnit(UnitType.Port, game.ref(coastX, 11), {}), + }, + ); + + const warshipTile = warship.tile(); + vi.spyOn(game, "getWaterComponent").mockImplementation((tile) => + tile === warshipTile ? 1 : 2, + ); + vi.spyOn(game, "hasWaterComponent").mockReturnValue(false); + + expect(tradeShip.owner().id()).toBe(player2.id()); + // Let plenty of time for warship to potentially capture trade ship + for (let i = 0; i < 10; i++) { + game.executeNextTick(); + } + + expect(tradeShip.owner().id()).toBe(player2.id()); + }); + test("Warship does not target trade ships that are safe from pirates", async () => { // build port so warship can target trade ships player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});