## Description:
The Doomsday Clock drain wiped a caught side's troops (and warship
health) to **zero**, an effective death sentence, so a brief dip below
the bar was unrecoverable. Floor the drain at **5% of each max**
(`drainFloorPercent`) for both troops and warships: a doomed side is
crippled, not eliminated, and recovers if it climbs back above the bar.
The rising territory bar still forces a finish (a 5%-strength side is
trivial to conquer, and the leader is drain-exempt).
- Config: `drainFloorPercent` (5), applied to both drains.
- Execution: clamp troop + warship-health removal to what sits above the
floor (integer-only, deterministic).
- HUD: the Collapsing rate is shown net of the floor.
- Tests: drain-to-zero / warship-scuttle cases updated to the floor +
added coverage.
## 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
## Description:
Replaces the "Disable alliances" toggle in the host and single-player
lobbies with a "Custom alliances" control: a toggle plus a minutes input
(0 to 15, step 1).
- 0 minutes disables alliances, same behavior as the old toggle.
- 1 to 15 sets the alliance duration in minutes.
How it works:
- Adds one game-config field, `customAllianceDuration` (minutes).
- `Config.allianceDuration()` uses it when set (and falls back to the
existing 5 minute default), and `Config.disableAlliances()` returns true
when it is 0.
- The legacy `disableAlliances` boolean is still read, so older/archived
configs keep working.
- Validation reuses the existing `parseBoundedIntegerFromInput` and
`toggle-input-card` helpers, so it behaves like the other numeric lobby
options (spawn immunity, max timer).
- The join-lobby screen shows "Alliances: {x}m", or "Alliances:
Disabled" at 0.
<img width="279" height="145" alt="image"
src="https://github.com/user-attachments/assets/5a608e18-3811-4eef-a3a6-9344aaf667fe"
/>
<img width="270" height="152" alt="image"
src="https://github.com/user-attachments/assets/9d8a4d30-51e7-4e82-8ce0-121b12af1c61"
/>
<img width="270" height="144" alt="image"
src="https://github.com/user-attachments/assets/a65b20fb-0db5-4657-a964-880fad7c864e"
/>
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
zixer._
Resolves #<add your approved & assigned issue number>
## Description:
Rebalances the Doomsday Clock so it acts as a **late-game
stalemate-breaker** rather than an early-game culler, softens how fast
it removes troops and warships, and makes the HUD countdown clearer.
**Clock schedule (all presets):**
- A flat **10-minute grace** at 0% required share — the early game is
decided by combat, not the clock.
- Then a 6-wave squeeze at accelerating levels (4 / 9 / 16 / 26 / 40 /
55%) with short pauses, reaching the final 55% at each preset's cap:
**45 / 35 / 25 / 15 min** for slow / normal / fast / veryfast.
- `WaveSchedule` `rampSeconds`/`pauseSeconds` are now **per-wave
arrays**, so the curve can be shaped (gentle early, steeper late)
instead of one uniform ramp. `requiredBasisPoints` and the HUD companion
`doomsdayClockWaveState` walk the per-wave segments in lockstep.
**Troop drain:** warn window `10s → 30s`, drain eased (`2%→5%` over
`90s`), so a caught side takes ~2 minutes to wipe instead of ~1.
**Warship attrition:** warships get their own gentler start plus a
**convex** decay curve — a ship caught when its side is first doomed
lasts about as long as troops, but the rate ramps up steeply so a side
at full attrition still loses its fleet in ~2s. Adds a `curveExponent`
argument to `doomsdayClockDrain` (1 = linear, used for troops; higher =
convex, used for ships).
**Determinism:** the drain curve is **integer-only** (fixed-point power,
no floats), so the floored per-tick loss is bit-identical on every
client in the lockstep sim. The linear troop path keeps its exact
existing integer form; only the convex warship path is reshaped.
**HUD countdown clarity:** the clock readout now shows a live countdown
in both states — `Will reach 16% in M:SS` while the bar is actively
climbing, and `Starts rising to 26% in M:SS` during a pause (previously
`Next 26% in …`, which read as if it jumped there instantly). Backed by
a new `secondsToTarget` field on the shared wave state so the sim and
HUD stay in agreement. All display text goes through `translateText()` /
`en.json`.
<img width="278" height="116" alt="image"
src="https://github.com/user-attachments/assets/e0be3d2c-bb88-46be-b344-34d63e4859bd"
/>
<img width="304" height="171" alt="image"
src="https://github.com/user-attachments/assets/da74bd05-0b9a-4aec-bbf5-e7380fbda88e"
/>
**Danger skull:** while a side is below the bar in the warn window, its
on-map skull now blinks progressively faster as the countdown runs out
(accelerating to the moment the drain begins), then holds steady once it
is actually draining — a clearer "you are about to be hit" cue.
Rationale: the previous schedule removed players heavily in the first
half of a match and could leave a drawn-out endgame. Holding the clock
at 0% early keeps the opening about fighting, and concentrating its
pressure in the back half reserves it for actually breaking stalemates.
Values are tuning starting points and easy to adjust in
`DoomsdayClock.ts` / the config defaults.
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
<add your Discord username>
## 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).
Resolves Issue #4463
## Description:
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.
### 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:
<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)
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
zixer._
## Description:
Warship veterancy! This is an idea inspired by the unit veterancy
feature of games like C&C: Red Alert 2 in which unit eliminations
increases the level of individual units. I've been trying to build this
mechanic for months with different ideas, and I finally landed on this
being one of the more balanced implementation.
Warships can earn up to three levels, represented by the gold bar
insignia in the bottom right of their warship sprite.
<img width="622" height="202" alt="image"
src="https://github.com/user-attachments/assets/a8c31a45-4ae9-41a9-b054-9c4a7f4ab1f1"
/>
A veterancy bar grants 20% health from the base amount, and a 20%
increase in shell damage applied _after_ the random damage roll. For
example, a level 3 warship will apply a 60% damage boost on top of the
random shell damage value (something between 200-325. If the random
value is 250, the final damage output will be `250 * 1.60 = 400`.
There are three ways to achieve a veteran level:
1. **Eliminate another warship:** any time a warship neutralizes another
warship, it immediately get's a veterancy increase.
https://github.com/user-attachments/assets/6a9e0958-5171-4ca3-94f6-9c2300a12f8b
2. **Eliminate transport boats:** Destroying 10 transport boats will
level a warship to the next veterancy bar.
https://github.com/user-attachments/assets/619ce0c0-033c-4e0b-9c64-b41eabaa791b
3. **Steal trade ships:** If the warship captures 25 trade ships, it
will earn a veterancy bar.
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
bijx
> **Before opening a PR:** discuss new features on
[Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small
improvements as
[issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose).
You must be assigned to an `approved` issue — unsolicited PRs will be
auto-closed.
**Add approved & assigned issue number here:**
Resolves#4396
## Description:
Makes the max rail length 1.4142 * the max station radius to be minimum
amount outside of the factory effect radius.
Bug:
<img width="1921" height="1078" alt="image"
src="https://github.com/user-attachments/assets/91b3b3fa-037a-4d9a-b06b-afe2fe2c8ea8"
/>
Fixed:
<img width="1922" height="1079" alt="image"
src="https://github.com/user-attachments/assets/3719cf73-bc41-494f-9d86-548f308f5896"
/>
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
tktk1234567
> **Before opening a PR:** discuss new features on
[Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small
improvements as
[issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose).
You must be assigned to an `approved` issue — unsolicited PRs will be
auto-closed.
**Add approved & assigned issue number here:**
Resolves#4323
## Description:
Made stations use euclidean distance for radius for checking if other
stations are close enough, removed redundant if check and unneeded
config
<img width="1920" height="1080" alt="Screenshot from 2026-06-18
14-19-48"
src="https://github.com/user-attachments/assets/a84f29f8-0cc1-46ea-9b96-3d70d6b0b20a"
/>
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
tktk1234567
## Description:
Relates to #3725
Adds a new **Impassable** terrain type that enables non-rectangular maps
and creates impassable barriers on the map. Painted with pure black
(`#000`) in the map editor's `image.png`.
**Encoding:** Impassable terrain is encoded in the binary format as
`isLand=1, magnitude=31` (previously unused). The Go map generator
detects `#000` pixels and produces this encoding. The map generator's
minimap downscaling gives impassable highest priority (Impassable >
Water > Land). Thumbnails render impassable as transparent so the map
picker background shows through.
**Rendering:** Impassable tiles render as the map background colour
(`rgb(60, 60, 60)`, matching `gl.clearColor` in `Renderer.ts`), making
them visually indistinguishable from the area outside the map quad. This
enables maps to appear non-rectangular.
**Gameplay restrictions:** Impassable terrain cannot be:
- Owned (`conquer()` throws)
- Attacked (`AttackExecution` skips impassable tiles in both `tick()`
and `addNeighbors()`)
- Nuked (targeting rejected in `nukeSpawn()`, blast radius filtered in
`tilesToDestroy()`)
- Spawned on (nations, human players, and structures all reject
impassable tiles)
- Converted to water (guarded in `WaterManager` and `setWater()`)
**Nuke trajectories:** Nuke trajectories cannot cross impassable
terrain, matching the existing map-border enforcement. This is checked
at launch time in `NukeExecution.tick()`. The client-side trajectory
preview turns red with a red X where the arc crosses impassable terrain
(reusing the existing SAM-intercept visual pipeline in
`NukeTrajectory.ts`). The nuke ghost preview is completely hidden when
hovering over impassable terrain (same as hovering outside the map).
https://github.com/user-attachments/assets/ff131146-9749-41e0-892a-617e5cd16c54
Impassable terrain is transparent on the thumbnail:
<img width="213" height="152" alt="Screenshot 2026-06-18 211640"
src="https://github.com/user-attachments/assets/ede16f8c-9239-4ab1-be5d-0ba81cce5e9e"
/>
Tested with water nukes, made sure there is no water depth gradient near
the impassable terrain, just like at the world border:
<img width="774" height="771" alt="Screenshot 2026-06-18 212348"
src="https://github.com/user-attachments/assets/4429069d-911b-48e8-91e3-7307d42c9397"
/>
Models used: GLM 5.2 and MiMo 2.5 Pro 😄
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Summary
The WebGL renderer was adapted from an external extension and carried a
lot of machinery this integration never uses (replay playback, its own
input/event system, a GL radial menu). This PR is two mechanical cleanup
passes with **no behavior change**: delete the dead code, then untangle
the `GameView` naming collision.
**78 files, +142 / −2,197.**
### Pass 1 — remove dead extension baggage
- **Replay/copy mode**: `FrameData.tileMode` was hard-coded `"live"`;
the copy branches in `frame/Upload.ts`, `UploadOptions` (never passed),
`applyFullFrame`/`applyFullTiles`/`applyDelta` on the facade and
`GPURenderer`, `HeatManager.resetForSeek`, and the seek-upload methods
on `TerritoryPass`/`TrailPass` were all unreachable. Also deletes
`types/Replay.ts`, `types/FrameSource.ts`, `types/GameUpdates.ts`,
`types/Game.ts` (imported only by the types barrel).
- **FrameEvents**: trimmed from 14 fields to the 3 actually populated
and read (`deadUnits`, `conquestEvents`, `bonusEvents`). The other 11
fed the extension's stats system and were never written or read here.
- **GL radial menu**: `RadialMenuPass`, its 4 shaders, and ~10 API
methods on facade + renderer had zero callers — the game uses the DOM/d3
radial menu in `hud/layers/RadialMenu.ts`. The pass was constructed and
drawn every frame for nothing.
- **Facade event system**: `GameViewEventMap` defined 10 event types
(`click`, `hover`, `scroll`, …) but only `contextrestored` was ever
emitted — input actually flows through `InputHandler` → EventBus →
controllers. Replaced the listener map with a single `onContextRestored`
callback and deleted `Events.ts`. Also fixed the stale header comment
claiming the facade handles user interaction.
- **Unused API surface**: removed ~20 facade/renderer methods with zero
callers (camera passthroughs like
`panTo`/`zoomTo`/`fitMap`/`screenToWorld`, hit-testing queries, SAM
replay setters, `setSelectedUnit`, `clearFx`/`setFxTimeFn`,
`onFrame`/`afterRender`/fps tracking).
Deliberately left alone: `Camera`'s pan/zoom primitives (building blocks
for a possible future camera unification) and the `timeFn` plumbing
inside the FX passes (deeply embedded as defaults; only the dead
renderer-level wrappers were removed).
### Pass 2 — untangle the three GameViews
- `render/gl/GameView.ts` → **`MapRenderer.ts`** (class `MapRenderer`).
Every importer was already aliasing it as `WebGLGameView` to dodge the
collision with the simulation-mirror `GameView` in `client/view/`, so
this removes aliasing rather than adding churn. `render/CLAUDE.md`
updated.
- Deleted the `src/core/game/GameView.ts` back-compat shim (its own TODO
asked for this). All 51 importers now import from `src/client/view/`
directly via a new 3-line barrel `view/index.ts`.
## Test plan
- `tsc --noEmit` clean, `eslint` clean
- Full test suite passes (1,385 + 65 server tests)
- Manual verification via headless Chromium: started a singleplayer game
and confirmed the renderer works end-to-end — terrain draws, spawn-phase
overlay shows, territories fill with borders after spawning, player
names/flags render, no renderer console errors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Resolves https://github.com/openfrontio/OpenFrontIO/issues/3445
## Description:
I copied the PR #3743 from @luctrate (Add army limit warning indicator
for team games) to this PR because he didn't respond to requested
changes but I thought it's important.
I expanded on it, now its a full help message system:
**Warnings (orange):**
- Army limit: shown in team games with donations when troops exceed 80%
of max
- Low troops: shown when troops drop below 1k (=> new noob player who
clicks too much)
<img width="764" height="251"
alt="582494157-cf19b13e-a0a9-44e4-8de8-86c007fe9c79"
src="https://github.com/user-attachments/assets/6b4996d9-1993-4d2c-98ba-afba17a5ca4d"
/>
**Info messages (blue):**
- Borders a traitor ally: "You can betray traitors without becoming a
traitor yourself" (Because its not obvious for new players)
- Borders an allied AFK player: "You can attack disconnected players
even if you are allied with them" (Because its not obvious for new
players)
- Borders an AFK teammate: "You can attack disconnected teammates"
(Because its not obvious for new players)
Info messages only appear when the player has not attacked the relevant
neighbor for at least 15 seconds, so they do not show up without reason.
<img width="524" height="141" alt="image"
src="https://github.com/user-attachments/assets/88d74661-d47e-45a7-9f91-d4f5361114b7"
/>
New "Help Messages" toggle in settings (default: on)
<img width="409" height="105" alt="image"
src="https://github.com/user-attachments/assets/24bc8bed-777b-4f72-9451-02116ac39db0"
/>
Implementation details:
- Border detection uses async borderTiles() refreshed every 1s, cached
in a Set of nearby player smallIDs
- Outgoing attacks are tracked per-target to compute the 15-second idle
threshold
- New armyLimitWarningThreshold() on Config (returns 0.8)
- All user-facing strings go through translateText() with en.json
entries
AI Model used: MiMo 2.5 Pro
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
**Add approved & assigned issue number here:**
Resolves#2549
## Description:
Themes are purely for the client's rendering, and the server doesn't
need context on them. This PR moves `Theme.ts` from
`src/core/configuration` to `src/client/theme` and moves affiliation
colors to `render-settings.json`.
This is to support the ability to add additional themes more quickly,
such as colorblind-friendly themes. No visible changes occur from this
refactor.
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
jetaviz
---------
Co-authored-by: Josh Harris <josh@wickedsick.com>
> **Before opening a PR:** discuss new features on
[Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small
improvements as
[issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose).
You must be assigned to an `approved` issue — unsolicited PRs will be
auto-closed.
**Add approved & assigned issue number here:**
Resolves#2668
## Description:
Makes radius that factories effect equal to the max distance that a
railroad can be and enable factory ghosts
<img width="1920" height="1080" alt="Screenshot from 2026-06-01
20-13-58"
src="https://github.com/user-attachments/assets/1e2f4de8-79fc-4034-b6ad-3c71255c0410"
/>
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
tktk1234567
`QuickChatExecution` had no cooldown, allowing a player to spam
quick-chat intents and flood a recipient's chat UI. This could bury
incoming alliance request notifications, preventing them from being seen
or accepted.
This fix mirrors the existing emoji cooldown pattern:
- Added `quickChatCooldown()` to `Config` (default: 30 ticks / 3 seconds)
- Added `canSendQuickChat(recipient)` and `recordQuickChat(recipient)`
to `Player` / `PlayerImpl`, tracking outgoing chats per recipient
- `QuickChatExecution.tick()` now checks `canSendQuickChat` before
displaying and records before the display calls (so the cooldown is
always written even if display throws)
The renderer kept a parallel CONSTRUCTION_DURATIONS table in
src/client/render/GameConstants.ts that had drifted from Config: port
showed as 20 ticks but the simulation builds it in 50, so the bar hit
100% and idled for 30 ticks. SAM/silo cooldown constants were also
stale (120/75 vs Config's 90/90), making the missile-readiness bar
slightly wrong too.
Delete GameConstants.ts entirely. Thread the Config instance through
WebGLGameView → GPURenderer → BarPass / FxPass / FxSpritePass /
WorldTextPass; passes call config.unitInfo(...).constructionDuration,
config.SAMCooldown(), config.deletionMarkDuration(), config.msPerTick()
directly. Add Config.msPerTick() since no method existed for it.
Move the visual-only NUKE_EXPLOSION_RADII (not a game-logic value)
into FxSpritePass where it's used.
## Description:
This is a refactor to simplify config handling.
Replaces the per-environment DevConfig/PreprodConfig/ProdConfig class
hierarchy with two static classes: ClientEnv (browser main thread, reads
from window.BOOTSTRAP_CONFIG) and ServerEnv (Node server, reads from
process.env). The four config classes are deleted, the abstract
DefaultServerConfig is gone, and DefaultConfig is renamed to Config.
The values that flow server → client (gameEnv, numWorkers,
turnstileSiteKey, jwtAudience, instanceId) used to be baked into the
hardcoded per-env classes. They're now real env vars on the server,
embedded into a single window.BOOTSTRAP_CONFIG object in index.html at
request time (alongside the existing gitCommit/assetManifest/cdnBase
globals, which moved into the same object), and read back by ClientEnv
on the client. The dev defaults previously hidden inside DevServerConfig
are now explicit in start:server-dev (NUM_WORKERS=2,
TURNSTILE_SITE_KEY=1x..., JWT_AUDIENCE=localhost, etc.) and in
vite.config.ts's html plugin inject.data. Production deploys plumb
NUM_WORKERS and TURNSTILE_SITE_KEY through deploy.yml (GitHub vars) into
the remote env file; JWT_AUDIENCE is derived from DOMAIN in deploy.sh.
The dynamic /api/instance endpoint is gone — INSTANCE_ID rides along in
BOOTSTRAP_CONFIG now.
ServerEnv is the only thing server code touches; ClientEnv is
browser-only. The two classes have intentional overlap (env, numWorkers,
jwtIssuer, gameCreationRate, workerIndex, etc.) since they derive
identical logic from different sources — there's a TODO in each to
consolidate via a shared helper later. The game-logic Config no longer
stores a ServerConfig/ClientEnv reference and its serverConfig() getter
is gone; the one caller (MultiTabModal) now reads ClientEnv.env()
directly. Worker init no longer carries server-config values since
nothing in the worker actually reads them.
## 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
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
evan
## Description:
Only mentioning removals/major updates/notable changes below, not all
minor upgrades.
### Removed:
- "@aws-sdk/client-s3": not used anywhere (was used in Archive.ts
previously)
- chai, "@types/chai", sinon-chai: not used anywhere, probably leftover.
Vitest uses a bundled version of Chai for its expect asserations under
the hood too.
- protobufjs, "@types/google-protobuf": not used anywhere, probably left
from evan's experiment with it? Removed from vite.config.ts too.
- "@types/jquery": not used anywhere, probably leftover
- sinon, "@types/sinon": not used anywhere just like chai, probably
leftover. And Vitest provides us with the same functionality.
- "@types/systeminformation": dependency systeminformation was removed
last year, this is an unneeded, deprecated and unmaintained remainder.
- vite-tsconfig-paths: removed, and removed the import and usage in
vite.config.ts and replaced it by adding `tsconfigPaths: true` to the
`resolve` block. Because of this message displayed on running the tests:
"The plugin "vite-tsconfig-paths" is detected. Vite now supports
tsconfig paths resolution natively via the resolve.tsconfigPaths option.
You can remove the plugin and set resolve.tsconfigPaths: true in your
Vite config instead."
- vite-plugin-static-copy: removed, we don't use it anymore (was used in
our vite.config.ts once,, probably before Vite natively supported
copying static assets via its publicDir configuration)
### Updated:
- color.js: v0.5 > v0.6, no breaking change affecting us
- cross-env: v7 > v10. It's a publicly archived repo since Nov 2025. But
before that he got it up-to-date from June 2025, porting to TS, dropping
old Node versions, dependencies etc. Seems still good to use for some
amount of time to come.
- dotenv: v16 > v17, now logs an informational message by default when
it loads an environment file. Can be disabled by using
dotenv.config({quite: true}) if needed.
- ejs: v3 > v5: security patches mostly. Vite still uses v3 btw.
- eslint: v9 > v10. Newly enabled rules by default:
'no-unassigned-vars', 'no-useless-assignment' and
'preserve-caught-error'. Mostly faster and minimum support moved to
higher node versions, which shouldn't be a problem.
- "@eslint/compat": v1 > v2. Minimum supported Node versions, which
should not be a problem.
- intl-messageformat: v10 > v11 no breaking changes that affect us
- jdom: v27 > v29. Faster. Most notably minimum support moved to higher
node v22 version, which should not be a problem. Also, see types/node,
kind of expecting v24 to be installed now.
- nanoid: from v3 to v5, no breaking changes that affect us
- "@opentelemetry/sdk-logs": now that addLogRecordProcessor is removed,
changed Logger.ts to pass an (empty) provider array directly to the
LoggerProvider constructor. Follows the changes in
https://github.com/open-telemetry/opentelemetry-js/pull/5588
- "@tailwindcss/vite": supports vite v8 from 4.2.2, and a fix for it in
4.2.4
- tailwindcss: supports vite v8 from 4.2.2
-- in 4.1.15 (we were already above this version) break-words was
deprecated in favor of wrap-break-word. But break-words, which we use in
15 places, will still work as expected
(https://github.com/tailwindlabs/tailwindcss/pull/19157). Same goes for
also deprecated "order-none".
- "@types/node": from v22 to v24, assuming most now use node 24
- vite v7 > v8:
-- is now on 8.0.10 so first bugs are out of it, while v8 itself also
fixed a big number of bugs.
-- in vite.config.ts, fixed Ts error/compilation issue by changing the
manualChunks option in build.rollupOptions.output to use the function
syntax, which is required by the updated types instead of the object
syntax.
- zod: no changes that affect us
### Prettier:
Updated only because of (new because of update?) Prettier errors for
files untouched in this PR originally:
- PathFinder.Parabola.ts
- WorkerMessages.ts
- ClanModal.handlers.test.ts
- ClanModal.rendering.test.ts
- CONTRIBUTING.md
- README.md
### ESLint:
Fixes needed to silence errors coming from newly enabled recommended
rules 'no-useless-assignment' and 'preserve-caught-error':
For 'no-useless-assignment' (default assignment never used because of
unreachable code or they are guaranteed to get a value, so they can be
undefinedat the start. Exception was AttackExecution, so made the
default value of 0 the default case in the switch statement):
- ClientGameRunner
- GameModeSelector
- NameBoxCalculator
- StructureDrawingUtils
- TerritoryLayer
- Diagnostics
- GameRunner
- ColorAllocator
- DefaultConfig
- AttackExecution
- AiAttackBehavior
- Worker.worker
- GamePreviewBuilder
For 'preserve-caught-error', disabled the rule here because the possible
fix `{cause: error}` was introduced in ES2022 while we're still on
target ES2020 currently:
- GameServer
- Privilege
_Error: The value assigned to 'gameMap' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'timeDisplay' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'scalingFactor' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'radius' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'teamColor' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'gl' is not used in subsequent statements.
(no-useless-assignment)
Error: The value assigned to 'power' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'tickExecutionDuration' is not used in
subsequent statements. (no-useless-assignment)
Error: The value assigned to 'selectedIndex' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'mag' is not used in subsequent statements.
(no-useless-assignment)
Error: The value assigned to 'speed' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'matchesCriteria' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'shouldContinue' is not used in subsequent
statements. (no-useless-assignment)
Error: The value assigned to 'description' is not used in subsequent
statements. (no-useless-assignment)
Error: There is no `cause` attached to the symptom error being thrown.
(preserve-caught-error)
Error: There is no `cause` attached to the symptom error being thrown.
(preserve-caught-error)_
All tests pass. TypeScript and ESLint errors resolved.
## 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
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
tryout33
---------
Co-authored-by: Copilot <copilot@github.com>
## Description
Re-enables Turnstile verification (was temporarily disabled in v31 to
diagnose intermittent `invalid-input-response` rejections) and moves the
verification call off the game servers.
Game servers no longer hold `TURNSTILE_SECRET_KEY` or hit
`challenges.cloudflare.com` directly. Instead they POST to
`${jwtIssuer}/turnstile` on the api-worker (authenticated with the
existing `apiKey`), which holds the secret and proxies to Cloudflare.
Shrinks blast radius and removes the secret from every game host + GH
Actions workflow.
Response from the api-worker is Zod-validated; null tokens short-circuit
to `rejected` locally.
## Please complete the following:
- [x] I have added screenshots for all UI updates (n/a — server only)
- [x] I process any text displayed to the user through translateText()
(n/a — no user-visible text)
- [ ] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Discord:
evanpelle
## Description:
The motivation is to prevent snowballing players from also gaining too
much gold by conquering other players
- Adds `conquerGoldAmount` to `Config`/`DefaultConfig`: returns 100% of
captured gold for bots/nations, 50% for human players
- Updates `GameImpl.conquerPlayer` to use this amount for the
conqueror's gold gain (the conquered player still loses their full gold)
## 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
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
evan
Part of [#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661)
(split into 3 PRs so they are not too large..)
## Description:
Part 2/3 of
[#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661).
This PR adds port-based healing and docking behavior:
- Passive healing near friendly ports
- Active docked healing pool scaled by port level and shared across
docked ships
- Docking radius and capacity-by-port-level behavior
- Waiting behavior near full ports until a slot opens
- Auto-undock once fully healed
For the active healing, it works like `ActiveHeal = (PortLevel * 5) /
DockedShipsAtThatPort`
Ex:
1 ship at level 1 port -> +5 HP/tick
1 ship at level 2 port → +10 HP/tick
2 ships at level 3 port → +7.5 HP/tick each
Includes regression tests covering healing math and docking/waiting
behavior.
## 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
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
zixer._
Part of #2661 (split into 3 PRs so they are not too large..)
## Description:
Part 1/3 of #2661.
This PR adds warship retreat basics, a blue retreating UI state, and
updates target priority.
Added:
- Retreat state handling
- Blue visual for retreating warships
- Target priority: transport > warship > trade
- Tests for retreat and target priority
Example video:
https://youtu.be/2hE2qeOeY48
Ship retreating:
<img width="630" height="488" alt="image"
src="https://github.com/user-attachments/assets/56d3e6d5-08af-453d-afe5-ee21dd6f3414"
/>
Ship healing:
<img width="483" height="311" alt="image"
src="https://github.com/user-attachments/assets/aeaf2239-bb81-444f-84ef-62dbcb48fddf"
/>
Back to being deployed:
<img width="585" height="358" alt="image"
src="https://github.com/user-attachments/assets/875828a2-8a24-4593-ac76-26426bb81057"
/>
## 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
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
zixer._
## Description:
I think this fixes the stalemate issue mentioned in the previous PR
without restoring the attack loss "bug". I tried making very small
adjustments (possibly too small?) to meet the goal of very gradually
changing the feel of the game.
If stalemate is still an issue, would recommend boosting the toAdd
parameter. I think the issue is not that attack losses are too high
relative to defender, but rather that large attackers get nerfed so much
by this formula. Going up to .8 from .73 is a roughly 10% boost in troop
growth for an attacker that is 4x the size of a defender. Not sure
anyone will notice, but it adds up over game time.
## 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
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
1brucben
## Description:
- Adds a "Host Cheats" toggle in the private lobby options section that
reveals a dedicated section with four host-only cheats: infinite gold,
infinite troops, gold multiplier, and starting gold
- Only the lobby creator receives the cheat effects in-game (checked via
`isLobbyCreator` in DefaultConfig)
- Joining players see active host cheats displayed as yellow badges in
the lobby UI
- Adds `hostCheats` optional object to `GameConfigSchema` and wires it
through the server config update whitelist
- Raises the intent size limit for `update_game_config` messages
(lobby-only, not stored in turn history) to prevent rate-limiter kicks
(I always got too-much-data-kicked after selecting "host cheats" lol)
<img width="861" height="525" alt="image"
src="https://github.com/user-attachments/assets/51e51ec4-c2e8-46ca-b258-11a93487964f"
/>
<img width="933" height="825" alt="image"
src="https://github.com/user-attachments/assets/5acbd38d-2097-42e1-ba78-0fb17d6afe82"
/>
## 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
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
Fixes population from a city being applied as soon as it is placed
instead of when it is fully built
## 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
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
babyboucher
## Description:
Adds a new `waterNukes` game config option that causes nuclear
detonations to convert land tiles into water instead of just leaving
fallout. When enabled, nuked land tiles are batched and converted to
water each tick, with full terrain metadata updates including:
- Ocean bit propagation from adjacent ocean tiles (BFS flood fill)
- Magnitude recomputation via BFS from remaining coastlines
- Shoreline bit fix-up in a 2-ring neighborhood around converted tiles
- Minimap terrain sync (majority-rule downsampling)
- Throttled water navigation graph rebuild (every 20 ticks) for ship
pathfinding
- Ship executions detect graph rebuilds and refresh their pathfinders
- TransportShips auto-retreat if their destination becomes water
- Water nuke craters use a smoothed angular noise ring with a
bounding-box scan instead of the regular per-tile random coin flip with
BFS, producing clean blob-shaped craters without scattered land pixels
that players would have to boat to individually
The `TerrainLayer` now incrementally repaints tiles that changed terrain
type, and tile update packets encode the terrain byte alongside tile
state so clients can reflect water conversions in real time.
When `waterNukes` is disabled, behavior is unchanged (fallout only).
Includes a new test suite (WaterNukes.test.ts) covering the conversion
pipeline, ocean propagation, magnitude recalculation, shoreline updates,
and minimap sync.
Also adds a new public game modifier for the special rotation.
### The only problem
A bit of lag on impact. But otherwise it works great and is fun. Maybe
needs some followup improvements if it gets merged.
I think its very cool in baikal / four islands team games. Chip away the
territory of your opponents.
Its also fun to turn The Box / Alps into a water map (its actually
possible to boat-trade then)
### Media
Video does not show the updated craters
https://github.com/user-attachments/assets/aed8bf08-0e94-4484-b997-4de11ae313d9
Updated craters (no tiny islands after impact):
<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/e896870b-bc9d-493d-8bc8-b3a5427d69d3"
/>
<img width="1472" height="920" alt="image"
src="https://github.com/user-attachments/assets/677065aa-0159-48cd-af44-a91b0f57adfc"
/>
<img width="1296" height="892" alt="image"
src="https://github.com/user-attachments/assets/886ffaba-541f-4e46-97c6-ce963f632fe0"
/>
## 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
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
This refactors client configuration loading to make the environment
split explicit.
The app currently has two different env concerns:
- the browser main thread needs the live runtime env to select API /
Turnstile / JWT settings
- the worker and game-logic path need a build-time env to select game
config behavior
Before this change, both responsibilities were hidden behind the same
loader, which made the intent unclear and caused confusion around the
worker fallback behavior.
This PR separates those paths explicitly:
- main-thread browser code now uses `getRuntimeClientServerConfig()`
- game creation and worker/game-logic code now uses
`getGameLogicConfig()`
- the build-time game-logic env is represented explicitly as
`GameLogicEnv`
## What Changed
- Added `GameLogicEnv` to model the build-time game config choice
explicitly.
- Added `getRuntimeClientServerConfig()` for live runtime browser config
from `window.BOOTSTRAP_CONFIG`.
- Added `getBuildTimeGameLogicEnv()` and
`getServerConfigForGameLogicEnv()` for build-time worker/game-logic
config.
- Renamed game config loading from `getConfig()` to
`getGameLogicConfig()` to reflect what it actually does.
- Updated browser call sites to use the runtime client config loader.
- Updated worker/game creation paths to use the game-logic config
loader.
- Updated config loader tests to cover both paths.
## Behavior
This keeps the current intended behavior, but makes it explicit:
- Runtime client env:
- comes from `window.BOOTSTRAP_CONFIG`
- controls live browser integration settings such as API origin,
Turnstile, and JWT audience/issuer
- Build-time game-logic env:
- comes from bundled `process.env.GAME_ENV`
- maps:
- `dev` -> dev game config
- `staging` -> default/prod game config
- `prod` -> default/prod game config
That means preprod/staging deployments can continue using prod game
logic while still using staging API/auth settings on the main thread.
## Why
The previous setup worked, but the naming and loader boundaries were
misleading:
- the same function was used for both runtime browser config and
worker/game config
- the worker fallback looked like an implementation detail instead of an
intentional architectural split
This change makes that intent visible in code without changing the
desired deployment behavior.
## 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
- [ ] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
DISCORD_USERNAME
## Description:
This reworks asset delivery and cacheability across the app and moves
non-bundled public resources onto immutable, content-hashed URLs.
Vite bundle outputs continue to live under `/assets/**` and remain
content-hashed by Vite. Public resources that were previously fetched
from stable paths in `resources/` now go through a custom hashed
namespace under `/_assets/**`, backed by a generated asset manifest that
is available to the server, browser, and worker runtime.
In parallel, the root app shell is now cacheable shared HTML instead of
request-time `no-store` HTML. Dynamic and live routes remain explicitly
uncached.
## Why
- Improve browser and Cloudflare cacheability for static assets.
- Remove query-string and release-version cache busting for
runtime-fetched assets.
- Allow unchanged public assets to keep the same URL across releases.
- Reduce avoidable work on `/` by serving a shared app shell instead of
rendering HTML on every request.
- Make cache behavior explicit instead of relying on mixed framework
defaults and file-extension heuristics.
## What Changed
### 1. Content-hashed public asset pipeline
- Added a build-time public asset manifest and hashing pipeline for
non-Vite resources.
- Production now emits hashed public assets under `/_assets/**`.
- Added runtime manifest loading for Node so server-rendered paths
resolve against built hashed files instead of rebuilding from source at
runtime.
- Emitted the runtime asset manifest as an ESM module for server
consumption.
Result:
- `/assets/**` = Vite-managed hashed bundle outputs
- `/_assets/**` = custom content-hashed public resources
### 2. Runtime asset URL migration
- Added a shared `assetUrl(...)` resolution path.
- Migrated runtime references away from query-string versioning and
stable source paths.
- Updated browser, worker, and server-side rendering paths to resolve
through the asset manifest.
- Moved map manifests, map binaries, thumbnails, sprites, sounds, fonts,
flags, icons, screenshots, and other runtime-fetched resources onto
hashed URLs.
### 3. Map and preview fixes
- Fixed directory and per-file map asset resolution so map manifest and
binary fetches resolve to the correct hashed URLs.
- Updated preview metadata and map thumbnail paths to use the hashed
asset namespace.
- Fixed runtime manifest loading in prod after deployment.
### 4. Explicit cache policies
- Added explicit immutable cache headers for:
- `/assets/**`
- `/_assets/**`
- worker-prefixed equivalents under `/wN/...`
- Added explicit `no-store` headers for live and dynamic APIs.
- Removed the old `/api/env` bootstrap request and baked `gameEnv` into
the HTML bootstrap instead.
### 5. Cacheable root app shell
- Refactored the root HTML path to serve a shared app shell with:
- `Cache-Control: public, max-age=0, s-maxage=300,
stale-while-revalidate=86400`
- `/` and the SPA fallback now serve shared cacheable HTML instead of
request-time `no-store` rendering.
- `/game/:id` remains dynamic and `no-store`, but now reuses the shared
shell before injecting preview tags.
### 6. Matchmaking instance handling
- Because the app shell is now cacheable, `INSTANCE_ID` was removed from
shared HTML.
- Added `/api/instance` as a temporary `no-store` runtime lookup used
only by matchmaking.
- This preserves correctness with the current random-per-boot
`INSTANCE_ID` model while keeping `/` cacheable, but it is not the
intended long-term design.
## Behavior Changes
### Asset URL contract
Production URLs for non-Vite public resources now change from stable
paths such as:
- `/maps/...`
- `/images/...`
- `/manifest.json`
to content-hashed paths under:
- `/_assets/...`
Examples:
- `/_assets/maps/<map>/manifest.<hash>.json`
- `/_assets/images/Favicon.<hash>.svg`
### Bootstrap/config
- `/api/env` is removed.
- `gameEnv` is now bootstrapped from HTML.
### HTML caching
- `/` and the SPA fallback are now cacheable shared HTML.
- `/game/:id` remains dynamic.
## Cache Matrix After This Branch
- `/_assets/**`: `public, max-age=31536000, immutable`
- `/assets/**`: `public, max-age=31536000, immutable`
- live `/api/**`: explicit `no-store`
- `/api/health`: explicit `no-store`
- `/api/instance`: explicit `no-store`
- `/game/:id`: explicit `no-store`
- `/` and SPA fallback: `public, max-age=0, s-maxage=300,
stale-while-revalidate=86400`
## Notes / Tradeoffs
- `/api/instance` is a temporary compromise. It exists because
`INSTANCE_ID` is currently random per boot, which is not safe to embed
into cacheable shared HTML.
- The current matchmaking flow still asks the client to provide
`instance_id` during `matchmaking/join`. That is functional, but it is
the wrong ownership boundary: instance selection should be handled by
the matchmaking service, not by the browser.
- The cleaner end-state would be:
- make `matchmaking/join` stop requiring `instance_id` from the client,
and let the matchmaking service select a healthy instance from worker
check-ins
- This branch makes the origin behavior edge-cache-friendly, but
Cloudflare still needs matching cache rules if HTML itself should be
cached at the edge.
## Validation
Verified during development with:
- `npx tsc --noEmit`
- `node node_modules\\vite\\bin\\vite.js build`
- `node node_modules\\vitest\\vitest.mjs run
tests/server/RenderHtml.test.ts tests/server/NoStoreHeaders.test.ts
tests/server/StaticAssetCache.test.ts
tests/core/configuration/ConfigLoader.test.ts`
Additional targeted tests added:
- `tests/AssetUrls.test.ts`
- `tests/core/game/FetchGameMapLoader.test.ts`
- `tests/core/configuration/ConfigLoader.test.ts`
- `tests/server/NoStoreHeaders.test.ts`
- `tests/server/StaticAssetCache.test.ts`
- `tests/server/RenderHtml.test.ts`
## Known Existing Warnings
The production build still reports pre-existing warnings that are not
addressed by this branch:
- inconsistent JSON import attributes for `resources/countries.json`
- inconsistent JSON import attributes for `resources/QuickChat.json`
- large chunk warnings from Vite
## Rollout Notes
- Cache rules should treat `/_assets/**` and `/assets/**` as immutable.
- Cloudflare will still classify HTML as dynamic after deploy unless
matching edge cache rules are configured for it.
## Follow-ups
- Remove `/api/instance` by changing `matchmaking/join` so the server
selects the target instance, or by making `INSTANCE_ID` deploy-stable if
the current contract must remain.
## 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
- [ ] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
DISCORD_USERNAME
## Description:
Increases the spawn immunity duration from 30s to 45s for maps with 5M
starting gold.
The previous 30s was too short - 45s gives players 15s longer than it
takes to build a SAM, allowing them to establish basic defenses before
becoming vulnerable.
## 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
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
Reduces `numSpawnPhaseTurns` from 300 to 100 (matching singleplayer
duration) when random spawn is active. Since players can't choose their
spawn location in random spawn mode, there's no need for the full
300-tick spawn phase.
## 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
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
Now that cities snap to existing rails, it's possible to line up dozens
of cities in a row, producing way too much gold. This PR reduces the
gold after each stop to prevent that. Gold only starts decreasing after
the 3rd city.
## 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
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
evan
## Description:
Rex had this idea: "It would be funny to have an option in private
lobbies to disable alliances."
I added it as an option.
Now people can choose to live in constant fear of their neighbors 😆
Also added two new public game modifiers for variety (only for the
special rotation):
- Alliances disabled (low probability)
- x2 gold multiplier (low probability)
Would be nice to squeeze this into v30, last minute?
## 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
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
I hope we can get this into v30?
The nation count is configurable now, just like the bot count.
Replaced the "Disable Nations" toggle with a nations slider (0–400) in
SinglePlayer and Host Lobby modals.
<img width="710" height="121" alt="Screenshot 2026-03-03 021952"
src="https://github.com/user-attachments/assets/c8d0f0c3-db51-4303-95fa-dbc770460ec2"
/>
Public games are staying exactly the same, this is just for singleplayer
and private lobby fun.
Youtubers could play HvN against 400 nations, for example.
Singleplayer enjoyers no longer have to play against 1 nation in HvN,
they can freely choose.
`GameConfig.disableNations: boolean` got replaced by `nations: number
(0-400, optional)`
`undefined` = map default,
`0` = disabled,
number = custom count
Nations slider defaults to the map's nation count, shows "(MAP DEFAULT)"
label when unchanged
Compact map toggle reduces nations to 25% when at default, restores when
toggled off (just like we already do with bots)
The nation count for HvN no longer automatically matches the human count
in singleplayer and private games, only in public games.
**What if there aren't enough nations configured for the map?**
We just use the HvN logic (Generate random nations)
### Warning
**This infra PR also needs to get merged:
https://github.com/openfrontio/infra/pull/263
Otherwise players can set 0 nations and get achievements.**
## 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
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
Preparation for nuke wars, for v30.
Next PR will be adding the nuke wars modifier for public games, but
Wonders https://github.com/openfrontio/OpenFrontIO/pull/3224 needs to be
merged first to avoid merge conflicts.
### 1. Disable boats setting
It's possible to disable `UnitType.TransportShip` now. Because they are
not needed in nuke wars and can even be annoying.
<img width="720" height="320" alt="image"
src="https://github.com/user-attachments/assets/661bc10d-b204-4b4f-b876-ee7c9b92de8c"
/>
### 2. Team spawn zones for random spawn
Maps can have `teamGameSpawnAreas` in their json file now.
Spawn areas are currently active if
- a supported map is chosen (Baikal Nuke Wars or Four Islands)
- a supported team size is chosen (2 teams on Baikal Nuke Wars or 2/4
teams on Four Islands)
- random spawn is enabled
## 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
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin