Skip to content

feat(sudden-death): battle-royale style zone gamemode#4469

Open
Zixer1 wants to merge 24 commits into
openfrontio:mainfrom
Zixer1:feat/sudden-death
Open

feat(sudden-death): battle-royale style zone gamemode#4469
Zixer1 wants to merge 24 commits into
openfrontio:mainfrom
Zixer1:feat/sudden-death

Conversation

@Zixer1

@Zixer1 Zixer1 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Resolves Issue #4463

Description:

An optional game mode that (almost) guarantees a finish instead of letting late-game
stalemates drag on.

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.

How it works

  • 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).

Notes for review

  • 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.

Testing

npm test, npm run lint, npx prettier --check ., npm run build-prod all pass.

UI:

Image

Dropdown between slow, normal, fast, very fast

Before zone:
Image

Zone started, player not affected the pannel also blinks orange for 10s:
Image

Player affected, grace period (Danger):
Image

Skull icon blinking over player (everyone sees it) - older screenshot, the clipping has been fixed
Image

Player affected, grace period ended (Draining):
Image

Skull icon no longer blinking, everyone can see you are in a state of decay, and troops are draining:
image

Skull is visible like alliances icon also on player tab
Image

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

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • 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._

Optional game mode that guarantees a finish. Once armed, every side (each
player in FFA, each whole team otherwise) must hold a rising share of the
map; a side below the bar is skulled and, after a short warn, its troops
bleed to zero.

- Threshold rises in waves: a grace, then linear ramps to each level with
  30s pauses. Levels track the ofstats FFA median (3/5/10/20/30%); the four
  speed presets (slow/normal/fast/very fast) change only the pace.
- Troop drain is a linear ramp on max capacity; ~1 min from caught to wiped.
- HUD: live bar vs target, wave/decay countdowns, red/orange cues, and an
  on-map skull (blinks in danger, steady while draining).
- Deterministic integer-only sim; covered by unit and integration tests.
@Zixer1 Zixer1 requested a review from a team as a code owner July 1, 2026 05:34
@Zixer1 Zixer1 self-assigned this Jul 1, 2026
@Zixer1 Zixer1 added the approved Approved for a PR, if you assigned to the issue. label Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds Sudden Death support across core game rules, player state flow, lobby controls, HUD/status rendering, localization, and tests. The game now tracks sudden-death state, computes wave and drain timing, applies it during execution, and renders the mode in client UI and status layers.

Changes

Sudden Death mode implementation

Layer / File(s) Summary
Core timing and execution
src/core/game/SuddenDeath.ts, src/core/Schemas.ts, src/core/configuration/Config.ts, src/core/game/Game.ts, src/core/game/PlayerImpl.ts, src/core/game/GameUpdates.ts, src/core/game/GameUpdateUtils.ts, src/core/execution/SuddenDeathExecution.ts, src/core/GameRunner.ts, src/server/GameServer.ts
Adds Sudden Death timing math, config schema/defaults, player state methods, update plumbing, execution logic, and server wiring.
Player state and status plumbing
src/client/render/types/Renderer.ts, src/client/view/PlayerView.ts, src/client/render/frame/derive/PlayerStatus.ts, src/client/view/GameView.ts, tests/GameUpdateUtils.test.ts, tests/client/render/frame/derive/*, tests/util/viewStubs.ts
Extends client-facing player state, status derivation, and render helpers with sudden-death fields and warning timing.
Lobby toggle and speed selector
src/client/components/GameConfigSettings.ts, src/client/HostLobbyModal.ts, src/client/SinglePlayerModal.ts, resources/lang/en.json
Adds Sudden Death toggle and speed selection in host and single-player flows, with matching localized labels.
Status icons and sidebar panel
resources/atlases/status-atlas-meta.json, src/client/render/gl/passes/name-pass/StatusIconProgram.ts, src/client/render/gl/passes/name-pass/Types.ts, src/client/render/gl/passes/name-pass/index.ts, src/client/hud/PlayerIcons.ts, src/client/hud/layers/GameRightSidebar.ts
Adds the sudden-death status icon, GPU status packing, player icon handling, and the right-sidebar Sudden Death panel.
Tests and coverage
tests/SuddenDeathExecution.test.ts
Adds unit and integration coverage for Sudden Death execution and helper math.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested labels: Feature, UI/UX, Backend

Suggested reviewers: evanpelle

Poem

A skull now glows when time runs thin,
The map must hold or troop loss begins.
Speed, warnings, bars, and modes align,
Sudden Death now marks the line.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: a new Sudden Death zone-style gamemode.
Description check ✅ Passed The description is directly about the new Sudden Death gamemode and its UI, rules, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Zixer1 Zixer1 added this to the Backlog milestone Jul 1, 2026
Resolve conflicts in HostLobbyModal.ts and GameServer.ts (sudden-death
config fields kept alongside upstream's new name-reveal/anonymize fields).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/client/render/frame/derive/player-status.test.ts (1)

25-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a direct test for the warn/drain boundary.

This helper now carries the new sudden-death fields, but the file still does not assert computePlayerStatus() below and at suddenDeathWarnTicks. That boundary drives the blink-vs-steady skull state in the name pass, so an off-by-one here would slip through.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/client/render/frame/derive/player-status.test.ts` around lines 25 - 36,
Add direct boundary coverage for sudden-death warn/drain in
computePlayerStatus(). Update the player status test file’s existing
computePlayerStatus assertions to explicitly verify behavior at
suddenDeathWarnTicks and just below it, so the warn-versus-drain state is pinned
down. Use the ps helper and the computePlayerStatus test cases to cover both
sides of the boundary and prevent off-by-one regressions in the name pass skull
state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/hud/layers/GameRightSidebar.ts`:
- Around line 305-314: Update the Sudden Death HUD math in GameRightSidebar so
it matches the core zero-land behavior instead of clamping land to 1. In the
section that computes land, requiredTiles, and the percentage values, preserve a
real land value and explicitly handle land <= 0 by treating required tiles and
displayed percentages as 0, rather than dividing by 1 and inventing ownership
percentages. Keep the fix localized to the sidebar rendering logic around
suddenDeathRequiredTiles/suddenDeathWaveState/sideTiles.

In `@src/client/render/gl/passes/name-pass/StatusIconProgram.ts`:
- Around line 171-190: The skull composition path in StatusIconProgram can still
fail after both images load, and the current catch only handles loading errors,
leaving atlasReady false. Update the composition/upload flow around the canvas
draw and upload(canvas) step so any drawing or upload failure falls back to
using the base atlas texture instead of dropping all status icons. Keep the
fallback localized to the status icon setup logic that builds the atlas for
suddenDeath.

In `@src/client/SinglePlayerModal.ts`:
- Around line 393-394: The hasOptionsChanged() logic in SinglePlayerModal is
still treating suddenDeathSpeed as a custom option even when suddenDeath is off,
which keeps the achievements warning active unnecessarily. Update the
option-change check so suddenDeathSpeed is only compared when suddenDeath is
enabled, and make the same conditional handling wherever the final config is
built in startGame() and the duplicated check around the other referenced block.

In `@src/core/configuration/Config.ts`:
- Around line 90-96: The default SuddenDeathDrain tuning in
SUDDEN_DEATH_DEFAULTS is too aggressive for the advertised one-minute wipe.
Retune the default values for enabled, warnSeconds, drainStartPercent,
drainMaxPercent, and/or drainRampSeconds so the default path lands near 60
seconds total, and verify the behavior through the SuddenDeathDrain config flow.
Add one regression test that uses the default Config/SUDDEN_DEATH_DEFAULTS path
and asserts the resulting drain timing matches the intended one-minute behavior.

In `@src/core/Schemas.ts`:
- Around line 250-257: The SuddenDeathConfigSchema currently validates
drainStartPercent and drainMaxPercent independently, so reversed values can
still pass. Add a cross-field validation on SuddenDeathConfigSchema to reject
configs where drainStartPercent is greater than drainMaxPercent, and keep the
existing per-field bounds intact. Use the SuddenDeathConfigSchema object as the
place to enforce the pairwise check before suddenDeathDrain() consumes it.

---

Nitpick comments:
In `@tests/client/render/frame/derive/player-status.test.ts`:
- Around line 25-36: Add direct boundary coverage for sudden-death warn/drain in
computePlayerStatus(). Update the player status test file’s existing
computePlayerStatus assertions to explicitly verify behavior at
suddenDeathWarnTicks and just below it, so the warn-versus-drain state is pinned
down. Use the ps helper and the computePlayerStatus test cases to cover both
sides of the boundary and prevent off-by-one regressions in the name pass skull
state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: caa3490a-1a5e-41f8-8050-c1c2d5675bb2

📥 Commits

Reviewing files that changed from the base of the PR and between 2794ab1 and b2374be.

⛔ Files ignored due to path filters (2)
  • resources/images/SuddenDeathSkull.svg is excluded by !**/*.svg
  • src/client/render/gl/shaders/name/status-icon.vert.glsl is excluded by !**/*.glsl
📒 Files selected for processing (29)
  • resources/atlases/status-atlas-meta.json
  • resources/lang/en.json
  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/components/GameConfigSettings.ts
  • src/client/hud/PlayerIcons.ts
  • src/client/hud/layers/GameRightSidebar.ts
  • src/client/render/frame/derive/PlayerStatus.ts
  • src/client/render/gl/passes/name-pass/StatusIconProgram.ts
  • src/client/render/gl/passes/name-pass/Types.ts
  • src/client/render/gl/passes/name-pass/index.ts
  • src/client/render/types/Renderer.ts
  • src/client/view/GameView.ts
  • src/client/view/PlayerView.ts
  • src/core/GameRunner.ts
  • src/core/Schemas.ts
  • src/core/configuration/Config.ts
  • src/core/execution/SuddenDeathExecution.ts
  • src/core/game/Game.ts
  • src/core/game/GameUpdateUtils.ts
  • src/core/game/GameUpdates.ts
  • src/core/game/PlayerImpl.ts
  • src/core/game/SuddenDeath.ts
  • src/server/GameServer.ts
  • tests/GameUpdateUtils.test.ts
  • tests/SuddenDeathExecution.test.ts
  • tests/client/render/frame/derive/nuke-telegraphs.test.ts
  • tests/client/render/frame/derive/player-status.test.ts
  • tests/util/viewStubs.ts

Comment thread src/client/hud/layers/GameRightSidebar.ts Outdated
Comment thread src/client/render/gl/passes/name-pass/StatusIconProgram.ts Outdated
Comment thread src/client/SinglePlayerModal.ts Outdated
Comment thread src/core/configuration/Config.ts Outdated
Comment thread src/core/Schemas.ts Outdated
@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Jul 1, 2026
Zixer1 added 5 commits July 1, 2026 01:57
drainStartPercent and drainMaxPercent were validated independently, so a
reversed pair (start > max) passed and produced a shrinking drain curve.
Add a cross-field refine so start <= max.
The panel clamped the land denominator to 1, inventing percentages (and
possible >100% ownership) when fallout removed all land. Pass the real
land to the shared math (which returns 0 there) and guard the percentages.
Only skull image loading fell back to the bare atlas; a failure in canvas
drawing or upload left atlasReady false and hid every status icon. Wrap
the whole compose path so any failure still uploads the bare atlas.
Turning Sudden Death on, changing pace, then off left hasOptionsChanged()
true (remembered speed != normal) even though startGame drops the mode.
Only count the pace when the mode is enabled.
Exercises the resolved SUDDEN_DEATH_DEFAULTS with real troop income and
asserts a full stack is wiped in ~1 minute from caught (warn + linear
drain), not the ~45s a no-income analysis predicts.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 1, 2026
Zixer1 added 2 commits July 1, 2026 21:17
A side's required share now scales with its member count: a team of N
must hold N x what a solo player holds (capped at the whole map). FFA
sides are size 1, so FFA is unchanged. Add suddenDeathSideRequiredTiles
as the shared source for both the sim and the HUD.
In team modes the panel shows the side's combined share as "Team <name>
<pct>%" instead of "You ...", and the threshold + wave percentages
reflect the headcount-scaled requirement.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/SuddenDeathExecution.test.ts (1)

305-319: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

New team-scaling test bypasses setup().

This new test builds its game via teamGame()FakeGame/FakePlayer rather than the real simulation harness. The path guideline for tests/**/*.test.ts calls for using setup() from tests/util/Setup.ts to exercise core simulation directly — a hand-rolled fake can silently drift from real PlayerImpl/Game semantics (e.g. team membership, tile bookkeeping) and mask regressions the real objects would surface.

The bottom two tests in this file (setup()-based) already give some real-sim coverage for wave/drain timing, so this isn't a correctness bug, but converting this specific scenario to setup() (or adding an equivalent real-sim assertion) would tighten confidence in the team-size scaling behavior.

As per path instructions: "Use the setup() helper from tests/util/Setup.ts to create test game instances and exercise core simulation directly."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/SuddenDeathExecution.test.ts` around lines 305 - 319, The new team-size
scaling test is using teamGame() with FakeGame/FakePlayer instead of the real
simulation harness. Update the test to use setup() from tests/util/Setup.ts (or
add an equivalent real-simulation assertion) so the SuddenDeathExecution
behavior is exercised through the actual PlayerImpl/Game path and not a
hand-rolled fake. Keep the same scenario and assertions, but locate the test by
its “scales the threshold by team size” description and replace the fake setup
with the shared setup() helper.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/SuddenDeathExecution.test.ts`:
- Around line 305-319: The new team-size scaling test is using teamGame() with
FakeGame/FakePlayer instead of the real simulation harness. Update the test to
use setup() from tests/util/Setup.ts (or add an equivalent real-simulation
assertion) so the SuddenDeathExecution behavior is exercised through the actual
PlayerImpl/Game path and not a hand-rolled fake. Keep the same scenario and
assertions, but locate the test by its “scales the threshold by team size”
description and replace the fake setup with the shared setup() helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a7f44d1d-d940-48de-8482-c524a3cde6c6

📥 Commits

Reviewing files that changed from the base of the PR and between 5e73854 and 45f4794.

📒 Files selected for processing (8)
  • resources/lang/en.json
  • src/client/SinglePlayerModal.ts
  • src/client/hud/layers/GameRightSidebar.ts
  • src/client/render/gl/passes/name-pass/StatusIconProgram.ts
  • src/core/Schemas.ts
  • src/core/execution/SuddenDeathExecution.ts
  • src/core/game/SuddenDeath.ts
  • tests/SuddenDeathExecution.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • resources/lang/en.json
  • src/client/SinglePlayerModal.ts
  • src/client/hud/layers/GameRightSidebar.ts
  • src/core/execution/SuddenDeathExecution.ts
  • src/core/Schemas.ts
  • src/client/render/gl/passes/name-pass/StatusIconProgram.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 2, 2026
The team readout doubled up as "Team <name>" (team names are already
"Team 1", "Red", etc.). Show "<name>: <pct>%" in the team's on-map
color instead.
@Zixer1

Zixer1 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Updated UI to work with the teams gamemode:
image

Teams gamemode now take the combined land % of the team (and requirement for land % is scaled depending on how large teams are)

@evanpelle evanpelle left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fable review:

Findings (most severe first)

  1. src/server/GameServer.ts:255 — Sudden death can never be turned off once enabled in a host lobby. When the host unchecks the toggle, HostLobbyModal.ts:1111 sends suddenDeath: undefined, which JSON.stringify drops entirely; the server's !== undefined guard then keeps the stored {enabled: true} config. The game starts with sudden death active while the host's UI shows it off. Sibling toggles clear via null (waterNukes: this.waterNukes ? true : null with a nullable schema) — suddenDeath needs the same, or send {enabled: false}. (Confirmed independently by four finder angles and direct code reading.)
  2. src/core/execution/SuddenDeathExecution.ts:51 — A player who dies while flagged keeps the mark forever. clearSuddenDeath() is only ever called on contenders, which filters on isAlive(), and no kill path resets markedSuddenDeathTick. Since suddenDeathTicks() is ticks - markedTick, it changes every tick, so diffPlayerUpdate emits a delta for the dead player on every tick for the rest of the game — and a local player who dies while flagged watches a permanently stuck "Draining" panel with an ever-growing rate while spectating (the sidebar has no alive gate; the name-pass skull is safe because PlayerStatus skips dead players).
  3. src/client/hud/layers/GameRightSidebar.ts:318 — Spectators and eliminated players get a permanent red-alert pulse. With me null or holding 0 tiles, yourPct is 0, so once the bar rises (requiredTiles > 0), nearDanger is true forever: the panel pulses red showing "Safe … You 0.0%" at someone with no territory stake. Needs a guard for no-player / dead-player states.
  4. src/core/game/PlayerImpl.ts:320 — Shipping the derived counter suddenDeathTicks defeats per-field diffing. The value changes every tick while flagged, so every skulled player forces 10 update deltas/second across the worker boundary when the client only needs 1 Hz. Sending the constant markedSuddenDeathTick instead (changes exactly on enter/clear) and deriving elapsed ticks client-side eliminates the churn — and would also cap the damage from finding 2.
  5. src/client/render/gl/passes/name-pass/StatusIconProgram.ts:130 — Runtime canvas compositing of the skull into the atlas adds a silent failure mode. If SuddenDeathSkull.svg 404s (new asset missing from the CDN bucket), the catch {} uploads the bare atlas with no warning, but the shader still activates slot 8 from pd4.w and samples atlas cell 10 — verified fully transparent in the committed PNG — so flagged players get a blank gap that mis-centers their whole status-icon row. The meta already reserves slot 10; baking the skull into the committed status-atlas.png deletes ~50 lines of loader/compositor/fallback (and the as any meta cast) and removes the failure mode.
  6. src/client/hud/layers/GameRightSidebar.ts:286 — sideTiles() hand-duplicates the sim's side-grouping rules. SuddenDeath.ts exists explicitly so "the sim and the HUD always agree," yet the FFA-vs-team grouping, bot/alive filters, and tile summing are re-implemented in the HUD (and the warn/drain derivation at line 314 is a second copy of the execution's logic). No numeric divergence today on the common path, but any tweak to one copy silently desyncs the displayed bar from who actually gets skulled. Move the side/tiles helper into SuddenDeath.ts.
  7. src/core/Schemas.ts:262 — The wire schema exposes four tunables nobody sends. warnSeconds, drainStartPercent, drainMaxPercent, drainRampSeconds (plus the cross-field refine) are only ever exercised by tests; both modals send just {enabled, speed}. They become permanent validated wire-protocol surface. Simpler: schema is {enabled, speed}, drain constants stay in SUDDEN_DEATH_DEFAULTS.
  8. src/client/components/GameConfigSettings.ts:189 — Feature-specific suddenDeathSpeed sentinel on the shared ToggleOptionConfig. renderOptionToggle branches on suddenDeathSpeed !== undefined into a bespoke card that re-implements the existing "toggle card with inner control" pattern (ToggleInputCard, used for Starting Gold). Either render via ToggleInputCard with a select variant or give the config a generic child-control slot, so the shared type stays feature-agnostic.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 2, 2026
Comment thread resources/lang/en.json Outdated
Comment thread resources/lang/en.json Outdated
Comment thread src/client/hud/layers/GameRightSidebar.ts Outdated
Comment thread src/core/execution/SuddenDeathExecution.ts Outdated
Comment thread src/core/execution/SuddenDeathExecution.ts Outdated
Comment thread src/core/game/Game.ts Outdated
Comment thread src/core/GameRunner.ts Outdated
Zixer1 added 2 commits July 1, 2026 22:10
When the host unchecked Sudden Death the lobby sent suddenDeath: undefined,
which JSON.stringify drops, so the server's "!== undefined" merge kept a
previously-enabled config and the game started with the mode still on. Send
{ enabled: false } instead so the off-state survives the wire and clears it.
Nothing clears markedSuddenDeathTick on death (the execution only touches
alive contenders), so a player who died while flagged stayed inSuddenDeath()
forever: a stuck "Draining" panel locally and a per-tick PlayerUpdate delta
(suddenDeathTicks keeps growing). Gate inSuddenDeath()/suddenDeathTicks() on
isAlive() so an eliminated player is cleanly not in sudden death.
Zixer1 added 5 commits July 1, 2026 22:27
The panel is a personal readout, but it rendered whenever the mode was on
with no isAlive gate. A spectator or 0-tile player has yourPct 0, so once the
bar rose nearDanger stayed true and the panel pulsed red "Safe ... You 0.0%"
forever. Return early when there is no alive local player.
diffPlayerUpdate excludes troops/gold/tiles (they travel on the compact stats
channel) but compared suddenDeathTicks, which grows every tick, so every
flagged player forced a full PlayerUpdate delta 10x/second. Ship the constant
markedSuddenDeathTick instead (changes only on enter/clear) and derive elapsed
ticks at read time in PlayerView and PlayerStatus. No more per-tick churn.
The zone now spares the side with the most tiles (the crown holder in FFA, the
top team otherwise). Sudden death culls the challengers toward the leader, so
the leader always keeps its army: the game can never freeze with every side
bled to zero, and the final wave squeezes out everyone but the leader for a
single winner. Ties broken by side order (deterministic).
The zone topped out at 30% (the FFA median), which up to three sides can hold
at once -> a possible top-of-map standoff. Add a 6th wave to 55%: only one side
can hold that, so combined with the crown exemption it squeezes out everyone
but the leader. Reached one cycle after 30% per preset (normal ~35 min).
…ompositing

The skull is now baked into status-atlas.png cell 10, so StatusIconProgram
loads the atlas plainly like every other icon. Removes the in-browser
compositor/fallback (~50 lines) and its silent failure mode (a missing SVG
would have left a blank gap that mis-centered the status-icon row).
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 2, 2026
Zixer1 added 5 commits July 1, 2026 23:20
Per maintainer review: "Sudden Death" implied instant elimination, but this is
a slow squeeze. Rename the mode to Doomsday Clock and the stages Safe/Danger/
Draining to Stable/Unstable/Collapsing. Renames every mention we added — code
identifiers, i18n keys, config + wire fields, and the source files/asset — so
the feature is one consistent name (no compatibility cost; it is all new).
Moves the ~160-line readout (render + sideStats/teamDisplayName/teamColor
helpers + styles) out of game-right-sidebar into a self-contained
<doomsday-clock-panel> element. Kept nested inside the sidebar so it still
stacks centered under the game timer; it hides itself (display:none) when off,
after a winner, or for a spectator/eliminated player.
GameRunner added DoomsdayClockExecution unconditionally (it self-gated in
tick). Guard registration on doomsdayClockConfig().enabled, matching the
pattern used for the other optional executions.
…filter

Per review: the mode is not OFM-specific (reword two comments), and
mg.players() already returns only alive players, so the contenders filter no
longer needs && p.isAlive(). Mirror that contract in the test's fake game.
The mode is not OFM-specific; reword the schema/config comments to match the
two the reviewer already flagged.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 2, 2026
The collapsing branch set no detail line, so "Rising to X%" / the wave
countdown vanished once troops started draining. Show the zone progress in
the collapsing state too (the bar keeps rising as you bleed).
No client ever sends the drain tunables (warnSeconds/drainStartPercent/
drainMaxPercent/drainRampSeconds); they were validated wire surface with no
sender. Drop them from DoomsdayClockConfigSchema and source them only from
DOOMSDAY_CLOCK_DEFAULTS. This supersedes the reversed-pair refine (12abd55) —
with the fields gone from the wire there is nothing to cross-validate.
@Zixer1

Zixer1 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

addressed fab 1-5, and 7
I am not so sure about 6 and 8, let me know if you want me to go ahead and implement them as initially requested by Fable
6. Skipped: the risky part (threshold/drain math) is already shared in DoomsdayClock.ts; only trivial "which side am I on" grouping differs and the sim/HUD use it differently, so a shared helper would add indirection without preventing a real bug
8. Skipped: ToggleInputCard is hardwired for a numeric , so a is a bigger rewrite than the small card it'd replace, and the shared styling is already centralized

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Approved for a PR, if you assigned to the issue.

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

2 participants