feat(doomsday-clock): battle-royale style zone gamemode (#4469)

Resolves Issue #4463

An optional game mode that (almost) guarantees a finish instead of
letting late-game
stalemates drag on.
Originally called sudden death, renamed to Doomsday clock

Once enabled, every side (each player in FFA, each whole team in team
modes)
must hold a rising share of the map. A side below the bar is skulled;
after a
short warn its troops bleed to zero, forcing consolidation to a winner.
- **Rising zone:** a grace period, then the required share ramps up
linearly to
each level with 30s pauses between (a battle-royale "zone"). Levels
track the
  ofstats FFA territory median (3/5/10/20/30%).
- **Four speed presets** (slow / normal / fast / very fast) change only
the pace:
  normal ends ~30 min, very fast ~15.
- **Troop decay:** a linear ramp as a % of max capacity, ~50s from
caught to zero
  (10s warn + ~50s ≈ 1 min total).
- **UI:** a HUD panel (live share vs target, wave/decay countdowns,
red/orange
cues) and an on-map skull above flagged players (blinks in danger,
steady while
  draining).
- Off by default; no effect on existing games. However, as discussed we
can add it to the modifier pool for public games to see how popular the
gamemode is vs normal play.
- Sim is deterministic (integer-only, in `src/core`), covered by unit +
  integration tests.
- One-line addition to `GameServer.updateGameConfig` so the setting
survives the
  host → server → client round-trip.
- Status is packed into the existing name-pass data slot (`pd4.w`: 0/1/2
=
none/danger/draining); the skull is composited into the icon atlas at
load.
`npm test`, `npm run lint`, `npx prettier --check .`, `npm run
build-prod` all pass.
<img width="243" height="100" alt="Image"
src="https://github.com/user-attachments/assets/c4c9eeb0-4feb-437d-9aac-b2786a841b74"
/>

Dropdown between slow, normal, fast, very fast

Before zone:
<img width="302" height="175" alt="Image"
src="https://github.com/user-attachments/assets/7359a1ea-4951-446d-a23c-0711fe06cc5d"
/>

Zone started, player not affected the pannel also blinks orange for 10s:
<img width="297" height="175" alt="Image"
src="https://github.com/user-attachments/assets/fcc565a5-d5d0-47a7-97ea-d0ba9d9ad899"
/>

Player affected, grace period (Danger):
<img width="314" height="170" alt="Image"
src="https://github.com/user-attachments/assets/ff96d21e-96f3-4ef9-8190-48eecc7aac0f"
/>

Skull icon blinking over player (everyone sees it) - older screenshot,
the clipping has been fixed
<img width="462" height="145" alt="Image"
src="https://github.com/user-attachments/assets/53899211-33b1-40e1-83f2-77f2096f0cad"
/>

Player affected, grace period ended (Draining):
<img width="360" height="159" alt="Image"
src="https://github.com/user-attachments/assets/4b226d57-da4d-4866-ab5f-db48e4ed1ea2"
/>

Skull icon no longer blinking, everyone can see you are in a state of
decay, and troops are draining:
<img width="732" height="146" alt="image"
src="https://github.com/user-attachments/assets/cd10fedb-6e87-4dfc-9fbf-55d3945a7901"
/>

Skull is visible like alliances icon also on player tab
<img width="558" height="81" alt="Image"
src="https://github.com/user-attachments/assets/6acdbe91-bdd0-40c7-942b-3990d4dae87f"
/>

(just UI example, best way to see it is to hop on a solo game and play
against AI)

- [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
regression is found:

zixer._
This commit is contained in:
Zixer1
2026-07-03 15:40:21 -07:00
committed by evanpelle
parent 580460c969
commit 63391212a3
33 changed files with 1593 additions and 17 deletions
+2 -1
View File
@@ -14,6 +14,7 @@
"embargo": 6,
"nukeRed": 7,
"nukeWhite": 8,
"allianceFaded": 9
"allianceFaded": 9,
"doomsdayClock": 10
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 99 KiB

+19
View File
@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 32 32">
<!-- Skull scaled to ~85% and nudged up so it clears the name below it. -->
<g transform="translate(16 13) scale(0.85) translate(-16 -15)">
<!-- cranium + jaw as one outline -->
<path d="M16 1.2C9.2 1.2 3.8 6.3 3.8 13.2c0 3.7 1.7 6.6 4 8.5.6.5.9 1.2.9 2v2.6c0 1.4 1.1 2.5 2.5 2.5h9.6c1.4 0 2.5-1.1 2.5-2.5v-2.6c0-.8.3-1.5.9-2 2.3-1.9 4-4.8 4-8.5C28.2 6.3 22.8 1.2 16 1.2Z"
fill="#F4F4F4" stroke="#191919" stroke-width="1.4" stroke-linejoin="round"/>
<!-- eye sockets -->
<ellipse cx="11.1" cy="13.3" rx="3.7" ry="4.3" fill="#191919"/>
<ellipse cx="20.9" cy="13.3" rx="3.7" ry="4.3" fill="#191919"/>
<!-- nasal cavity -->
<path d="M16 17.6l-2 3.7h4z" fill="#191919"/>
<!-- teeth -->
<g stroke="#191919" stroke-width="1.4" stroke-linecap="round">
<line x1="13" y1="24.4" x2="13" y2="28.2"/>
<line x1="16" y1="24.4" x2="16" y2="28.4"/>
<line x1="19" y1="24.4" x2="19" y2="28.2"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+21
View File
@@ -381,6 +381,25 @@
"discord_user_header": {
"avatar_alt": "Avatar"
},
"doomsday_clock": {
"collapsing": "Collapsing -{rate}/s",
"decay_in": "Decay in {secs}s",
"final": "Final zone, hold {pct}%",
"growing": "Rising to {pct}%",
"hold": "Hold ≥ {pct}%",
"next_wave": "Next {pct}% in {time}",
"stable": "Stable",
"title": "Doomsday Clock",
"unstable": "Unstable",
"you": "You {pct}%",
"your_team": "{team}: {pct}%"
},
"doomsday_clock_speed": {
"fast": "Fast",
"normal": "Normal",
"slow": "Slow",
"veryfast": "Very Fast"
},
"error_modal": {
"connection_error": "Connection error!",
"copied": "Copied!",
@@ -698,6 +717,7 @@
"disable_alliances": "Disable alliances",
"donate_gold": "Donate gold",
"donate_troops": "Donate troops",
"doomsday_clock": "Doomsday Clock",
"empty_team": "Empty",
"empty_teams": "Empty Teams",
"enables_title": "Disable Units",
@@ -1193,6 +1213,7 @@
"bots_disabled": "Disabled",
"compact_map": "Compact Map",
"disable_alliances": "Disable alliances",
"doomsday_clock": "Doomsday Clock",
"enables_title": "Disable Units",
"gold_multiplier": "Gold multiplier",
"gold_multiplier_placeholder": "2.0x",