feat(playlist): add the Doomsday Clock to the public modifier rotation (#4589)

## Description:

Adds the anti-stall **Doomsday Clock** to the public "special" modifier
rotation, so it rolls into public games like the other spice modifiers,
with a lobby badge.

- `MapPlaylist`: new `isDoomsdayClock` ticket in `SPECIAL_MODIFIER_POOL`
(weight 4, ~20% of special games). When rolled, enables `doomsdayClock`
at a speed picked per game (slow/normal/fast/veryfast).
- `PublicGameModifiers` (type + schema): `isDoomsdayClock` flag drives
the badge.
- `getActiveModifiers` + `en.json`: a "Doomsday Clock" badge/label.
- Test covering the badge wiring.

## 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
This commit is contained in:
Zixer1
2026-07-16 10:38:51 -07:00
committed by evanpelle
parent d3ecb5f073
commit 682fabd77b
6 changed files with 59 additions and 2 deletions
+2
View File
@@ -1147,6 +1147,8 @@
"crowded": "Crowded",
"disable_alliances": "Alliances Disabled",
"disable_alliances_label": "Alliances",
"doomsday_clock": "Doomsday Clock",
"doomsday_clock_label": "Doomsday Clock",
"gold_multiplier": "x{amount} Gold Multiplier",
"hard_nations": "Hard Nations",
"nukes_disabled": "Nukes Disabled",
+6
View File
@@ -220,6 +220,12 @@ export function getActiveModifiers(
badgeKey: "public_game_modifier.water_nukes",
});
}
if (modifiers.isDoomsdayClock) {
result.push({
labelKey: "public_game_modifier.doomsday_clock_label",
badgeKey: "public_game_modifier.doomsday_clock",
});
}
return result;
}
+1
View File
@@ -281,6 +281,7 @@ export const GameConfigSchema = z.object({
isSAMsDisabled: z.boolean().optional(),
isPeaceTime: z.boolean().optional(),
isWaterNukes: z.boolean().optional(),
isDoomsdayClock: z.boolean().optional(),
})
.optional(),
nations: z
+1
View File
@@ -143,6 +143,7 @@ export interface PublicGameModifiers {
isSAMsDisabled?: boolean;
isPeaceTime?: boolean;
isWaterNukes?: boolean;
isDoomsdayClock?: boolean;
}
export interface UnitInfo {
+30 -2
View File
@@ -74,7 +74,8 @@ type ModifierKey =
| "isNukesDisabled"
| "isSAMsDisabled"
| "isPeaceTime"
| "isWaterNukes";
| "isWaterNukes"
| "isDoomsdayClock";
// Each entry represents one "ticket" in the pool. More tickets = higher chance of selection.
// Weights are roughly informed by the community "favorite modifier" poll.
@@ -92,8 +93,19 @@ const SPECIAL_MODIFIER_POOL: ModifierKey[] = [
...Array<ModifierKey>(1).fill("isSAMsDisabled"),
...Array<ModifierKey>(1).fill("isPeaceTime"),
...Array<ModifierKey>(4).fill("isWaterNukes"),
...Array<ModifierKey>(4).fill("isDoomsdayClock"),
];
// Speeds the Doomsday Clock can roll at when it lands in the rotation. Picked
// per game (see getSpecialConfig) so the pacing varies instead of always being
// the same preset.
const DOOMSDAY_ROTATION_SPEEDS = [
"slow",
"normal",
"fast",
"veryfast",
] as const;
// Maps where water nukes have a higher chance on top of the normal pool
// Water nukes are especially fun here
const WATER_NUKES_BOOSTED_MAPS: ReadonlySet<GameMapType> = new Set([
@@ -254,6 +266,7 @@ export class MapPlaylist {
isSAMsDisabled,
isPeaceTime,
isWaterNukes,
isDoomsdayClock,
} = poolResult;
if (boostWaterNukes) {
isWaterNukes = true;
@@ -280,7 +293,8 @@ export class MapPlaylist {
!isNukesDisabled &&
!isSAMsDisabled &&
!isPeaceTime &&
!isWaterNukes
!isWaterNukes &&
!isDoomsdayClock
) {
excludedModifiers.push("isCrowded");
const fallback = this.getRandomSpecialGameModifiers(
@@ -297,6 +311,7 @@ export class MapPlaylist {
isSAMsDisabled,
isPeaceTime,
isWaterNukes,
isDoomsdayClock,
} = fallback);
({ isHardNations } = fallback);
}
@@ -353,7 +368,19 @@ export class MapPlaylist {
isSAMsDisabled,
isPeaceTime,
isWaterNukes,
isDoomsdayClock,
},
// Rolled into the rotation: enable the anti-stall clock at a speed picked
// per game so the pacing varies across the presets.
doomsdayClock: isDoomsdayClock
? {
enabled: true,
speed:
DOOMSDAY_ROTATION_SPEEDS[
Math.floor(Math.random() * DOOMSDAY_ROTATION_SPEEDS.length)
],
}
: undefined,
startingGold,
goldMultiplier,
disableAlliances: isAlliancesDisabled ? true : undefined,
@@ -557,6 +584,7 @@ export class MapPlaylist {
isSAMsDisabled: selected.has("isSAMsDisabled") || undefined,
isPeaceTime: selected.has("isPeaceTime") || undefined,
isWaterNukes: selected.has("isWaterNukes") || undefined,
isDoomsdayClock: selected.has("isDoomsdayClock") || undefined,
};
}
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { getActiveModifiers } from "../../src/client/Utils";
// The doomsday clock is part of the public "special" modifier rotation, so an
// active isDoomsdayClock modifier must surface a lobby badge like every other
// rotation modifier.
describe("doomsday clock public modifier", () => {
it("surfaces a doomsday-clock badge when the modifier is active", () => {
const mods = getActiveModifiers({ isDoomsdayClock: true });
expect(mods).toHaveLength(1);
expect(mods[0].badgeKey).toBe("public_game_modifier.doomsday_clock");
expect(mods[0].labelKey).toBe("public_game_modifier.doomsday_clock_label");
});
it("omits the badge when the modifier is absent", () => {
expect(getActiveModifiers({})).toHaveLength(0);
expect(getActiveModifiers(undefined)).toHaveLength(0);
});
});