fix(doomsday): keep doomed warships on patrol instead of idling at ports (#4597)

Follow-up to #4587 (the 5% drain floor).

## Description:

A side below the Doomsday Clock bar cannot repair its navy: warship
healing is suppressed in `WarshipExecution.healWarship` so the decay
actually lands. Since #4587 floors the drain at 5% of max instead of
sinking ships to zero, warships now survive at 5% health, which is below
the 75% `warshipRetreatHealthPercent` retreat threshold. The result was
that every doomed warship peeled off to the nearest port, docked, and
then never healed, sitting idle and out of combat, instead of the
intended crippled-but-still-fighting state.

This gates the repair-retreat on the Doomsday Clock, mirroring the
existing heal gate: a doomed warship no longer starts a retreat, and any
in-flight retreat or dock is cancelled so it returns to patrol. It keeps
fighting at the floor until its side climbs back above the bar (healing
resumes) or it is destroyed in combat.

No config, schema, or UI change. Deterministic (a boolean check over
existing player/warship state). Covered by three new tests in
`tests/Warship.test.ts` (doomed stays on patrol, non-doomed still
retreats, doomed undocks).

## Please complete the following:

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

zixer._
This commit is contained in:
Zixer1
2026-07-16 10:38:51 -07:00
committed by evanpelle
parent 682fabd77b
commit 225743206d
2 changed files with 63 additions and 0 deletions
+15
View File
@@ -71,6 +71,16 @@ export class WarshipExecution implements Execution {
this.healWarship();
this.handleManualPatrolOverride();
// A doomed side cannot repair its navy (see healWarship), so retreating to
// a port only pulls a warship out of the fight to idle there forever. Undock
// or abort any retreat and keep patrolling until the side recovers.
if (
this.warship.owner().inDoomsdayClock() &&
this.warship.warshipState().state !== "patrolling"
) {
this.cancelRepairRetreat();
}
if (this.warship.warshipState().state === "docked") {
if (this.currentRetreatPort() === undefined) {
this.cancelRepairRetreat();
@@ -167,6 +177,11 @@ export class WarshipExecution implements Execution {
if (this.warship.warshipState().state !== "patrolling") {
return false;
}
// A doomed side cannot repair (see healWarship), so there is nothing to
// retreat for; stay on patrol instead of idling at a port.
if (this.warship.owner().inDoomsdayClock()) {
return false;
}
const manualMoveRetreatDisabledDuration = 50;
if (
this.mg.ticks() - this.lastManualMoveTickRetreatDisabled <
+48
View File
@@ -5,6 +5,7 @@ import {
Player,
PlayerInfo,
PlayerType,
Unit,
UnitType,
} from "../src/core/game/Game";
import { TileRef } from "../src/core/game/GameMap";
@@ -105,6 +106,53 @@ describe("Warship", () => {
expect(warship.health()).toBe(maxHealth - 8);
});
// A crippled warship normally retreats to a port to heal (threshold 75%).
function crippledWarshipWithPort(): Unit {
const maxHealth = game.config().unitInfo(UnitType.Warship).maxHealth;
if (typeof maxHealth !== "number") {
throw new Error("warship has no maxHealth");
}
player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
const warship = player1.buildUnit(
UnitType.Warship,
game.ref(coastX + 1, 10),
{ patrolTile: game.ref(coastX + 1, 10) },
);
game.addExecution(new WarshipExecution(warship));
player1.conquer(game.ref(coastX, 10)); // owns a tile -> isAlive() for the clock
game.executeNextTick();
// Drop to ~5%, well below the 75% retreat threshold.
const low = Math.max(1, Math.floor(maxHealth * 0.05));
warship.modifyHealth(low - warship.health());
expect(warship.warshipState().state).toBe("patrolling");
return warship;
}
test("Crippled warship retreats to a port when not doomed", async () => {
const warship = crippledWarshipWithPort();
executeTicks(game, 10);
// Baseline: it leaves patrol to retreat/dock at the port.
expect(warship.warshipState().state).not.toBe("patrolling");
});
test("Doomed warship stays on patrol instead of retreating", async () => {
const warship = crippledWarshipWithPort();
player1.enterDoomsdayClock();
executeTicks(game, 10);
// A doomed side cannot heal, so the warship must not peel off to a port.
expect(warship.warshipState().state).toBe("patrolling");
});
test("Doomed warship undocks and returns to patrol", async () => {
const warship = crippledWarshipWithPort();
executeTicks(game, 10);
expect(warship.warshipState().state).not.toBe("patrolling");
// Falling under the bar mid-repair sends it back to patrol, not idling.
player1.enterDoomsdayClock();
game.executeNextTick();
expect(warship.warshipState().state).toBe("patrolling");
});
test("Warship captures trade if player has port", async () => {
const portTile = game.ref(coastX, 10);
player1.buildUnit(UnitType.Port, portTile, {});