feat(doomsday-clock): decay warships alongside troops for doomed sides (#4499)

## Description: 

Follow-up to #4469.

The Doomsday Clock drains a doomed side's troops but leaves its navy
untouched, so a coastal or island turtle can sit below the bar
indefinitely on warship defense, exactly the stall the clock is meant to
break.

This decays the warships of a flagged (sub-threshold, non-leader) side
on the same ramp as its troops:

- Each warship loses a percentage of its (veterancy-adjusted) max health
per second, reusing `doomsdayClockDrain`, so the fleet and the army
bleed in lockstep and reach zero together (~55s from full at the default
rate).
- Destruction passes **no attacker**, so it routes through
`UnitImpl.delete` as an environmental loss: no kill credit, no
boat-destroy stats, no veterancy granted. Scoring integrity is
preserved.
- Healing is suppressed for a flagged owner
(`WarshipExecution.healWarship` early-returns), so the decay actually
sinks the fleet instead of being out-healed at a port. Inert when the
mode is off, since the mark is never set.
- The leader's fleet is spared, same as its troops.

No new config: warships reuse the existing drain curve. No HUD change,
since warships count as part of the side's forces alongside troops.

Tested: 4 new unit tests (same-ramp decay, no-kill-credit destruction,
leader spared, warn-window grace), the full `DoomsdayClockExecution` and
`Warship` suites, the whole test suite (1784 passing), `build-prod`, and
a headless full-game sim run (resolves cleanly with the decay live,
deterministic).
This commit is contained in:
Zixer1
2026-07-03 15:07:28 -07:00
committed by GitHub
parent 2a1381b41e
commit 66063d6178
5 changed files with 179 additions and 1 deletions
+5
View File
@@ -94,6 +94,10 @@ const DOOMSDAY_CLOCK_DEFAULTS = {
drainStartPercent: 2, // starts bleeding at once (already beats troop income)
drainMaxPercent: 6,
drainRampSeconds: 50, // ramps LINEARLY to the max over this long
// Warships bleed on the same start + ramp but to a much higher ceiling than
// troops, so a fleet at full attrition sinks in ~2s (50% of a ship's max
// health per second) instead of riding out the gentle troop rate. Ships only.
warshipDrainMaxPercent: 50,
};
export class Config {
@@ -130,6 +134,7 @@ export class Config {
drainStartPercent: d.drainStartPercent,
drainMaxPercent: d.drainMaxPercent,
drainRampSeconds: d.drainRampSeconds,
warshipDrainMaxPercent: d.warshipDrainMaxPercent,
};
}
spawnImmunityDuration(): Tick {
+26 -1
View File
@@ -9,6 +9,7 @@ import {
Player,
PlayerType,
Team,
UnitType,
} from "../game/Game";
/**
@@ -43,6 +44,13 @@ export class DoomsdayClockExecution implements Execution {
const mg = this.mg;
const cfg = mg.config().doomsdayClockConfig();
if (!cfg.enabled) return;
// Warships bleed on the same start + ramp as troops but toward a much higher
// ceiling (warshipDrainMaxPercent), so a fleet at full attrition sinks in
// ~2s. Only the max differs from the troop drain.
const warshipDrainCfg = {
...cfg,
drainMaxPercent: cfg.warshipDrainMaxPercent,
};
const elapsed = mg.elapsedGameSeconds();
// Humans and Nations are subject to it; the small map bots are not (the
@@ -94,12 +102,29 @@ export class DoomsdayClockExecution implements Execution {
m.enterDoomsdayClock();
const secondsUnder = Math.floor(m.doomsdayClockTicks() / 10);
if (secondsUnder >= cfg.warnSeconds) {
const secondsPastWarn = secondsUnder - cfg.warnSeconds;
const chunk = doomsdayClockDrain(
mg.config().maxTroops(m),
secondsUnder - cfg.warnSeconds,
secondsPastWarn,
cfg,
);
m.removeTroops(chunk); // caps at current troops
// The navy bleeds on the same ramp but toward warshipDrainCfg's far
// higher ceiling (see above), so a doomed side's fleet is scuttled
// fast at full attrition. A percentage of each warship's (veterancy-
// adjusted) max health; passing no attacker makes each destruction
// environmental, never a credited kill (see UnitImpl.delete). Healing
// is suppressed for flagged owners in WarshipExecution.healWarship so
// the decay actually lands.
for (const ws of m.units(UnitType.Warship)) {
ws.modifyHealth(
-doomsdayClockDrain(
ws.maxHealth(),
secondsPastWarn,
warshipDrainCfg,
),
);
}
}
}
} else {
+4
View File
@@ -122,6 +122,10 @@ export class WarshipExecution implements Execution {
private healWarship(): void {
const owner = this.warship.owner();
// A doomed side (below the Doomsday Clock bar) cannot repair its navy, so the
// decay in DoomsdayClockExecution actually sinks warships instead of being
// out-healed at a port. Inert when the mode is off: the mark is never set.
if (owner.inDoomsdayClock()) return;
const passiveHealing = this.mg.config().warshipPassiveHealing();
const passiveHealingRange = this.mg.config().warshipPassiveHealingRange();
const passiveHealingRangeSquared =