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-13 12:15:30 -07:00
committed by GitHub
parent 290d6922eb
commit 9b69b7f422
2 changed files with 63 additions and 0 deletions
+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";
@@ -107,6 +108,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, {});