mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-06 10:54:21 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user