Previously structures capped at iconScale = 1.0 once zoomed past
iconScaleFactorZoomedOut, staying at a fixed pixel size no matter how
far you zoomed in. They felt overlaid on the map instead of part of it.
Add a third zoom band controlled by structure.iconGrowZoom. Past this
threshold iconScale = uZoom / iconGrowZoom — structures grow with the
canvas (world-anchored, fixed map-area coverage). Plumbed via the
uIconGrowZoom uniform on StructurePass.
Default 7 keeps normal play unchanged; only kicks in at deep zoom.
Structures collapse to a dot when zoomed out past dotsZoomThreshold.
The dot scale was hardcoded to 1.0 / 2.5 (≈0.4) in structure.vert.glsl.
Promote it to a render setting (`structure.dotScale`) so it's tunable
alongside iconSize / dotsZoomThreshold. Default 0.4 preserves current
behavior. Plumbed via uDotScale uniform on StructurePass.
DynamicUILayer subscribed to ConquestEvent and drew a floating "+gold"
text on capture, but the WebGL renderer now draws the same popup via
ConquestPopupPass (fed through applyConquestEvents in uploadFrameData).
The result was two popups stacked on every capture.
Drop the canvas2D handler. Bonus events and unit-death FX use the same
duplication pattern but are left intact for now — separate change when
the WebGL versions are verified visually.
Two issues with mounting the WebGL renderer alongside canvas2D:
1. One-frame camera lag. WebGL had its own RAF loop independent of
canvas2D's. When the user panned, WebGL's RAF could fire before
canvas2D's syncCamera ran, drawing with stale camera state.
Fix: pass a capturing raf/caf to the renderer so its loop never
actually schedules itself; invoke the captured frame callback
synchronously from canvas2D's onPreRender hook, after setCameraState.
Both renderers now lock-step on a single RAF.
2. Layout thrashing. syncCamera read glCanvas.clientWidth/Height every
frame, forcing a synchronous layout flush — ~11% CPU under Layout.
Fix: cache canvas dimensions and update via ResizeObserver. Canvas
size changes are rare; the cached values are accurate between
resizes.
PlayerView/UnitView now wrap renderer-shaped state objects (PlayerState,
PlayerStatic, UnitState) directly instead of holding engine wire types.
GameView owns a long-lived FrameData object kept in sync each tick:
players/units/tiles/trail/railroad are mutated in place; derived buffers
(playerStatus, relationMatrix, allianceClusters, nukeTelegraphs,
attackRings) and events are recomputed in a final populateFrame() pass.
The renderer reads gameView.frameData() and the same byte-identical
state objects PlayerView/UnitView wrap. WebGLFrameBuilder shrinks from
~270 to ~70 LOC: palette management + a single uploadFrameData() call,
no per-frame UnitState allocation on the hot path.
Wiring: maxPlayers=1024 on RendererConfig (pre-sizes NamePass/palette/
relation matrix textures); NamePass disabled so HTML NameLayer remains
the only on-screen player names.
Also: 39 new tests covering PlayerView/GameView/FrameData behavior;
replace .data field access in three layer call sites with accessor
methods (betrayals(), type(), getTraitorRemainingTicks()).
Resolves:
https://discord.com/channels/1284581928254701718/1502285978121801851/1502285978121801851
## Description:
Replace the requeue button-click workaround with a direct
`open-matchmaking` event
Keep consuming only the `requeue` URL parameter while preserving other
query params and hash
https://github.com/user-attachments/assets/7922b4ec-1686-484b-8ce1-b417896ddc44
## 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:
aotumuri
## Summary
- Tier upgrade/downgrade in the Store. The Subscriptions tab now shows
all tiers including the user's current one. Other tiers swap "Subscribe"
→ "Switch" when the user already has a sub, and clicking them calls the
new `POST /subscriptions/@me/change-tier` endpoint with a
direction-aware confirm (upgrade charges prorated diff now, downgrade
gives account credit).
- Owned-tier card renders a **Current Plan** badge in place of the
purchase button. Resolution logic in `resolveCosmetics` now reads
`userMeResponse.player.subscription.tier` (with flare fallback) and
marks that tier as `owned`.
- AccountModal's `<subscription-panel>` reworked into a proper
two-column layout:
- **Left**: tier name, `$X.XX/mo` price, description, daily Pu/Caps
amounts.
- **Right**: status badge (Active / Renews date / Cancels date),
`[Manage] [Change Tier]` button row, `[Cancel]` centered underneath.
When `cancelAtPeriodEnd === true`, the row collapses to a single
`[Reactivate]` button (opens the Stripe portal).
- New `<o-button size="xs">` variant (`py-2 px-3 text-xs`) for the
compact panel buttons.
- Store dollar-purchase price label now supports an optional suffix
(`/mo` for subs only) via a `priceSuffix` prop plumbed through
`CosmeticContainer` → `PurchaseButton`.
- `Api.ts` gains `changeSubscriptionTier(tierName)` with the same
401-handling pattern as the existing subscription helpers.
<img width="1114" height="728" alt="Screenshot 2026-05-14 at 7 09 20 PM"
src="https://github.com/user-attachments/assets/688f83d5-4010-4580-9214-6885af8ec98e"
/>
<img width="1038" height="276" alt="Screenshot 2026-05-14 at 7 09 33 PM"
src="https://github.com/user-attachments/assets/458197f5-a0d4-4c32-bc55-31e5679629b5"
/>
<img width="887" height="286" alt="Screenshot 2026-05-14 at 7 09 55 PM"
src="https://github.com/user-attachments/assets/8149ed82-89cc-4bbe-83de-3614f886b331"
/>
## Discord
evan
## Description:
Renames TheStraits map. The people that suggested this map told me they
would prefer a more specific name for the map, rather than the generic
one it has right now. So im renaming it into Danish Straits
This map is for v32, it has not been released, it should be fine to
rename
## 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:
tri.star1011
## Description:
Adds map "Northwest Passage", map of the Canadian Arctic Archipelago ,
Greenland and surroundings. "Northwest Passage" (NWP) is the sea lane
between the Atlantic and Pacific oceans
(https://en.wikipedia.org/wiki/Northwest_Passage) .
21 default nations, based on the towns of the region.
This map uses the brand new additionalNations feature made by FloPinguin
https://github.com/openfrontio/OpenFrontIO/pull/3902 . Adds 39 extra
nations for a total of 60 nations (so that in gamemodes like Humans vs
Nations all the nations have names of real places)
Comparison:
- Map with default nations
- Map with extra named nations, tested by raising the number of nations
in Solo
<img width="1050" height="412" alt="image"
src="https://github.com/user-attachments/assets/12ed94f1-0615-4fb3-b0d0-dcecb65006ea"
/>
<img width="1089" height="436" alt="image"
src="https://github.com/user-attachments/assets/6e7c11bf-7382-4e36-9433-229a9d463b68"
/>
Terrain source from OpenTopography, already credited in CREDITS.md
## 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:
tri.star1011
## Description:
The GitHub translation key was incorrectly categorized under news even
though it is used on the main page.
This changes its category to main.
## Please complete the following:
- [ ] 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:
aotumuri
## Description
Adds a modal URL router so modals can be opened, deep-linked, and
bookmarked via the hash. URLs of the form `#modal=<name>&tab=<key>&...`
open the named modal and pass remaining keys as args to `onOpen`. The
reverse direction also syncs: opening a modal via the UI updates the
URL, closing it clears the hash, and switching tabs updates `&tab=`.
Builds on the BaseModal refactor from #3923.
### What's new
**`ModalRouter.ts`** — small registry + two-way sync helper.
- `register(name, { tag, pageId? })` declares a modal as router-managed
- `routeFromHash()` parses `#modal=...` and dispatches to
`modal.open(args)`
- `syncOpened/syncClosed/syncTab` push state back into the URL via
`history.replaceState` (no history entries)
- A `routingFromUrl` flag prevents URL→modal→URL feedback loops
- Unknown modal names silently strip the hash
**`BaseModal`** — opt-in URL sync via a `routerName` property.
- When set, BaseModal calls into
`modalRouter.syncOpened/syncClosed/syncTab` from `open` / `close` /
`setActiveTab`
- Modals that own their own URL state (lobby modals) just leave
`routerName` undefined
**`Main.ts`** — registers all routable modals and wires the router.
- `handleUrl()`: adds a `modalRouter.routeFromHash()` branch after the
path-based lobby join
- `onHashUpdate`: when the hash is router-managed, routes via the router
instead of tearing down lobby state
### Routable modals
13 inline modals: store, settings, leaderboard, clan, account, help,
news, language, single-player, ranked, troubleshooting,
territory-patterns, flag-input.
Excluded by design: join-lobby, host-lobby (own URL state via
`/game/<id>`), matchmaking (no URL state).
### Example uses
- Deep link to store flags tab: `/#modal=store&tab=flags`
- Settings keybinds tab: `/#modal=settings&tab=keybinds`
- Cosmetics.ts now redirects to `#modal=store&tab=packs` when a
hard-currency purchase fails for insufficient plutonium (after the
alert), so users can top up directly
### URL behavior
- `replaceState` everywhere — no history entries added when modals open
/ close / switch tabs
- Browser back/forward still works for the existing path-based game flow
- `hashchange` events are router-aware so external hash changes (back
button, manual edit) correctly switch between routed modals without
tearing down lobby state
## Please complete the following:
- [x] I have added screenshots for all UI updates _(no visual changes;
smoke-tested in dev)_
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file _(no new user-visible strings)_
- [ ] I have added relevant tests to the test directory _(no test
coverage; manually tested URL load, UI open, tab switch, close,
hashchange, insufficient-plutonium redirect)_
- [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:
DISCORD_USERNAME
## Description
Refactors the modal system so that `BaseModal` owns the `<o-modal>`
shell rendering, tab state, and lifecycle. Modal subclasses now provide
content via small hook methods (`renderHeaderSlot()`, `renderBody(tab)`,
`modalConfig()`) instead of each rebuilding the `<o-modal>` template and
inline-mode branching.
This sets up the foundation for a future modal URL router (e.g.
`#modal=store&tab=flags`), which will be a follow-up PR.
### What changed
**`BaseModal`** — `src/client/components/BaseModal.ts`
- Now renders the `<o-modal>` shell itself; subclasses no longer
duplicate it
- Owns `activeTab` state and dispatches per-tab rendering via
`renderBody(tab)`
- Single `modalConfig()` method returns `{ title?, tabs?, hideHeader?,
hideCloseButton?, alwaysMaximized?, maxWidth? }`
- Uniform `open(args?)` / `close(args?)` interface; subclasses interpret
args in `onOpen(args)` / `onClose(args)`
- Tabbed modals can lazy-load via `onTabEnter(tab)` lifecycle hook
- Re-entrancy guard on `open()` so `showPage()` re-invocations don't
clobber state set by the outer call
- Initial tab defaults to first entry in `modalConfig().tabs` so the
active tab is highlighted on first open
**17 modals migrated** to the new shape:
- Tabbed: Store, UserSetting, Leaderboard, Clan
- Non-tabbed: FlagInput, Account, TokenLogin, News, TerritoryPatterns,
Troubleshooting, SinglePlayer, Matchmaking, RankedModal, Help, Language
- Lobby: JoinLobbyModal, HostLobbyModal (kept their `confirmBeforeClose`
/ `closeAndLeave` / `closeWithoutLeaving` methods)
Per-modal diffs are mostly mechanical:
- Drop the `<o-modal>` wrapper template and the `if (this.inline) return
content` branch
- Drop the inner `<div class="${this.modalContainerClass}">` wrapper
(shell styling now lives on `<o-modal>`)
- Move header content into `renderHeaderSlot()` so it lives in the
sticky header area
- Convert `super.open()`/`super.close()` overrides into
`onOpen(args)`/`onClose(args)` hooks
- For tabbed modals: drop subclass `@state activeTab`, manual
`handleTabChange`, and the `render()` switch — all owned by BaseModal
now
**Other changes:**
- `Store`: in affiliate mode (`#affiliate=X`), tabs are hidden and a
single combined grid of purchasable affiliate items is shown
- `Main.ts`: `joinModal.open(lobbyId, lobbyInfo)` callsites converted to
the new `open({ lobbyId, lobbyInfo })` shape
### Follow-up
Modal URL router (`#modal=X&tab=Y&...`) is a separate PR on top of this
foundation.
## Please complete the following:
- [x] I have added screenshots for all UI updates _(no visual changes;
smoke-tested in dev)_
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file _(no new user-visible strings)_
- [ ] I have added relevant tests to the test directory _(no test
coverage; tested in browser)_
- [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
## Summary
- Add a **Subscriptions** tab to the Store. Each tier renders as a
`<cosmetic-button>` with description, daily Pu/Caps amounts, and a
Stripe checkout button driven by the existing `createCheckoutSession`
flow.
- Show the player's active subscription in the **Account modal** via a
new `<subscription-panel>` Lit component (status badge, period-end /
cancel-at-period-end, daily currency breakdown).
- **Manage** button opens the Stripe billing portal in a new tab (`POST
/subscriptions/@me/portal`).
- **Cancel** button (hidden once `cancelAtPeriodEnd === true`) calls
`POST /subscriptions/@me/cancel` after a `confirm()` prompt, then
invalidates the userMe cache and refetches.
- Block re-purchase: clicking Subscribe when the user already has a
`subscription:*` flare alerts "Already subscribed" before opening
checkout (upgrade/downgrade flows are out of scope for now).
- Schema additions:
- `CosmeticsSchema.subscriptions: Record<string, SubscriptionSchema>`
(optional) in `src/core/CosmeticSchemas.ts`.
- `UserMeResponse.player.subscription: { tier, status, currentPeriodEnd,
cancelAtPeriodEnd } | null` in `src/core/ApiSchemas.ts`.
- Translations: new `store.*` and `account_modal.sub_*` keys in
`resources/lang/en.json` (English only — Crowdin handles the rest).
-
<img width="942" height="313" alt="Screenshot 2026-05-14 at 1 13 05 PM"
src="https://github.com/user-attachments/assets/3d28df13-9e03-49f0-bee8-a25f9ad0c420"
/>
<img width="545" height="439" alt="Screenshot 2026-05-14 at 1 13 32 PM"
src="https://github.com/user-attachments/assets/b413b275-d6f2-40dc-9230-d68cd11fb07a"
/>
## Discord
evanpelle
iOS Safari has ignored the `user-scalable=no` viewport hint since iOS
10, so two-finger pinch still zooms the whole page and can softlock the
in-game HUD. Intercept WebKit's non-standard `gesturestart`,
`gesturechange` and `gestureend` events at `document` and call
`preventDefault()` so the page stays put. The game's own pinch-to-zoom
on the map canvas is driven by pointer events (InputHandler) and is
unaffected; browsers that do not fire GestureEvent treat the listeners
as a no-op.
Resolves#2330
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #(issue number)
## Description:
Describe the PR.
## 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:
### 1. `SPECIAL_MODIFIER_POOL` rebalanced
Ticket weights adjusted to roughly track the community "favorite
modifier" poll
<img width="486" height="724" alt="Screenshot 2026-05-11 210740"
src="https://github.com/user-attachments/assets/bb1d2461-beb3-41c0-8d7b-b604db5fc033"
/>
- `isRandomSpawn`: 2 to 4
- `goldMultiplier`: 4 to 6
- `isWaterNukes`: 3 to 4
- `startingGold25M`: 1 to 3
- `startingGold5M`: 5 to 4
- `startingGold1M`: 3 to 2
### 2. New `SPECIAL_TEAM_MAPS` config
Replaces the hardcoded per-map branches in `getTeamCount` and
`buildMapsList`. Each entry maps a `GameMapType` to its preferred
`TeamCountConfig`. Shared constants:
- `SPECIAL_TEAM_FORCE_CHANCE = 0.75` (probability of overriding the
random team weights roll)
- `SPECIAL_TEAM_FREQ_MULTIPLIER = 2` (frequency boost in the team
playlist)
Current entries: Baikal (2), FourIslands (4), Luna (2). Behavior
preserved for the existing maps, but adding another special team map is
now a one-line entry.
### 3. New `FULL_LAND_MAPS` config (TheBox, Alps)
- Water nukes forced on 75% of the time in the special rotation
(overrides `WATER_NUKES_BOOSTED_MAPS`, which still applies its 50% boost
to FourIslands, Baikal, Luna, ArchipelagoSea). Because they make a lot
of fun on these two maps.
- The `isPortsDisabled` modifier is excluded unless water nukes is
boosted on, since ports are pointless on full-land maps. Because this
happened:
<img width="516" height="292" alt="image"
src="https://github.com/user-attachments/assets/cd9ce31d-25d0-4b35-a8ba-bb3ec1c02b70"
/>
### 4. Misc
- Renamed `frequency` constant to `FREQUENCY` for consistency with other
module-level constants.
### 5. Exclude `isNukesDisabled` on special team maps in team mode
On `SPECIAL_TEAM_MAPS` (FourIslands, Baikal, Luna) in team mode, the
`isNukesDisabled` modifier is now excluded from the pool. Otherwise an
extreme warship spam will follow.
## 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:
Allows the Alliance hotkey to extend an alliance as expected by
https://discord.com/channels/1284581928254701718/1503351192921571409
## 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:
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:
**Adds "The Straits" map:**
A map located around Denmark and the many surrounding straits: Kattegat,
Skagerrak and the Danish straits (thus the name, meant to be a creative
name like "Between Two Seas" and "Gateway to the Atlantic"). This map is
themed in the early 1900s, the nations/NPCs are traditional and
historical regions of Sweden-Norway, Denmark and the Germany.
Relatively small map with ~700k land tiles, similar to World
Inspired by this Discord thread with nearly 20 upvotes:
https://canary.discord.com/channels/1284581928254701718/1482089104110911634/1482089104110911634
<img width="365" height="506" alt="image"
src="https://github.com/user-attachments/assets/5ee16218-34c0-4b8b-9f9b-d33f219760b0"
/>
## 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:
tri.star1011
## Description:
Replaced hard coded defaults with defaultKeybinds
## 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:
Guard `ws.send()` in `endTurn()` with a `readyState === OPEN` check to
prevent sending messages to WebSocket connections that have already
closed.
Without this guard, broadcasting to a client whose connection closed
between ticks can throw an exception and crash the game loop.
## 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:
barfires
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves#3847
## Description:
Makes a setting to turn off go to player on spawn.
Most players i have played with are really annoyed about this feature. I
understand that some people like it, which is why i have made it default
on. But i feel like we should be able to turn it off.
## Please complete the following:
- [ ] 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
- [ ] 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:
FrederikJA
## Description:
Adds an optional `additionalNations` array to map manifests (info.json /
manifest.json), used as a name pool when a game requests more nations
than the map defines (HvN, private lobbies, solo games).
Suggested by mapmaker PatrickPlaysBadly.
When the requested nation count exceeds `nations.length`:
1. The deficit is filled by random picks from `additionalNations`
(collisions with manifest names are skipped).
2. If `additionalNations` still does not cover the deficit, the
remainder is generated procedurally as before.
Each entry supports `name`, optional `flag` and optional `coordinates`.
If `coordinates` are provided, the picked nation gets a spawn cell
(otherwise it spawns like the procedurally generated ones, with no fixed
location).
`Nation.flag` is also relaxed to optional, since many existing manifest
entries already omit it.
## 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
Resolves#1041
## Description:
Remove the singleplayer spawn countdown so the game starts when the
player spawns, spawn nations immediately after player spawn, and align
game timer/max-timer timing with the new start point.
Added a singleplayer regression test for spawn-immunity timing
(GameImpl.test.ts) and updated spawn-phase loop tests to use gameType:
GameType.Public where singleplayer behavior is not under test (e.g.
MIRV/AI/Spawn/WinCheck-related suites), eliminating inSpawnPhase()
timeout hangs after the new singleplayer start logic.
https://github.com/user-attachments/assets/c07a585f-1153-490e-88ca-a91fc7ae5756
## 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:
aotumuri
## Description:
Format the map lists in Main and MapPlaylist by alphabetical order. Many
maps were already standarized like this, but many recent ones werent.
Ideally, future maps should be added this way too.
Also ran npm run format in these 2 files, as they were not formatted
correctly before.
## 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:
tri.star1011
## Description:
Map of the Taiwan and the Chinese Mainland.
Team heavy map like Baikal and Hormuz. Terrain Source from
OpenTopography, already credited
<img width="1800" height="1511" alt="image"
src="https://github.com/user-attachments/assets/45954469-8199-4882-9efe-899c5df87ce4"
/>
I also took the chance to standarize and sort alphabetically the map
lists in main.go and MapPlaylist.ts.
## 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:
tri.star 1011
NOTE: If the map gets added, please give contributor to
crunchybbbbb_59469 for this map on Discord. Every file was made by him,
his PR just had weird bugs that didnt allow the PR to be review
automatically
Original PR: https://github.com/openfrontio/OpenFrontIO/pull/3853 by
crunchybbb2-hash
## Description:
Fixes a typo in `GameServer.ts` - `handline` should be `handling` in the
websocket error log message.
## 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:
barfires
## Description:
improvements to clan ui.
<img width="788" height="290" alt="image"
src="https://github.com/user-attachments/assets/736ca147-bff4-44d8-8180-7b80a85556fe"
/>
added "expand all" and new collapsible sections.
<img width="787" height="550" alt="image"
src="https://github.com/user-attachments/assets/deb2f813-854b-46a9-a767-52c4f749f30f"
/>
which changes to collapse all when expanded
also adds more info about team (d,t,q,2,3,4,5,6,7 team)
## 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:
w.o.n
## Description:
improvements to clan ui.
<img width="788" height="290" alt="image"
src="https://github.com/user-attachments/assets/736ca147-bff4-44d8-8180-7b80a85556fe"
/>
added "expand all" and new collapsible sections.
<img width="787" height="550" alt="image"
src="https://github.com/user-attachments/assets/deb2f813-854b-46a9-a767-52c4f749f30f"
/>
which changes to collapse all when expanded
also adds more info about team (d,t,q,2,3,4,5,6,7 team)
## 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:
w.o.n
## Description:
Loading spinner:
https://github.com/user-attachments/assets/9033b707-7499-4a52-b0c6-d96d8f331ee3
## 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:
fixed issue when leaving and trying to rejoin a clan without a reload
## 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:
w.o.n
## Description:
fixed issue when leaving and trying to rejoin a clan without a reload
## 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:
w.o.n
Add a thinner hover ring for the ranked/create/join action buttons via a new --shadow-action-card-hover variable, apply the same ring to the flag and skin selectors, and split the solo button's hover scale to expand mostly vertically so it doesn't clip horizontally.
## Description:
Refactors tab handling out of the individual modal components and into
the base o-modal component. Tabs are now declared by passing tabs,
activeTab, and onTabChange props, and a new named header slot pins
consumer-supplied content above the tabs. This standardizes the modal
tab look.
<img width="1089" height="290" alt="Screenshot 2026-05-06 at 12 17
33 PM"
src="https://github.com/user-attachments/assets/08d5a039-0aef-4aa7-b972-1e43b8723685"
/>
## 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
Resolves [#3815
](https://github.com/openfrontio/OpenFrontIO/issues/3815)
## Description:
Migrates the five `mg.neighbors(tile)` call sites in
`src/core/execution/AttackExecution.ts` to the existing zero-allocation
`forEachNeighbor(tile, cb)` helper (`src/core/game/GameImpl.ts:1107`).
`neighbors()` allocates
a fresh `TileRef[]` of length up to 4 on every call; `forEachNeighbor`
invokes a callback with no allocation. The helper is already used in
`PlayerExecution`, `GameImpl.updateBorders`, and similar hot paths —
this PR
finishes that rollout in the hottest attack loop.
Affected sites in `src/core/execution/AttackExecution.ts`:
| Original line | Context |
| --- | --- |
| 277 | `tick()` border check around `tileToConquer` |
| 326 | `addNeighbors()` outer neighbor loop |
| 335 | `addNeighbors()` nested `numOwnedByMe` count |
| 370 | `handleDeadDefender()` border test |
| 375 | `handleDeadDefender()` neighbor scan |
Notes:
- `handleDeadDefender` previously narrowed `this.target` from `Player |
TerraNullius` to `Player` via the early-return check. Inside an arrow
callback that narrowing isn't preserved by TypeScript, so the function
now
captures `target: Player` once after the check. Same pattern as
`tick()`'s `targetPlayer` cache.
- For loops that previously used `break` or `.some()`, I used a small
flag variable to short-circuit the callback body. With at most 4
neighbors per tile the extra callback invocations are negligible
compared to the
eliminated array allocation.
Behavioral guarantees:
- Iteration order is identical (`forEachNeighbor` enumerates the same
tiles in the same order as `neighbors()`).
- No RNG, no float math, no wire-format changes.
- Determinism preserved.
Verification:
- `npm test` — all 994 tests pass across 104 files. The 22
attack-related tests in `Attack.test.ts`, `AttackStats.test.ts`, and
`AiAttackBehavior.test.ts` exercise the migrated code paths end-to-end.
- `npx tsc --noEmit` — clean for the changed file.
- `npx prettier --check` — clean.
Briefly flagged in `#development` per the contribution guidelines before
opening.
## 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
- [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:
sxndrexe
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves#3174
## Description:
This PR implements a keybind for retaliation against incoming non-bot
attacks (Players and Nations). It currently will target the most recent
(last in array) incoming attack when the keybind is pressed, choosing
the minimum of attack ratio or incoming attack troop size to retaliate
with. This correlates with the last displayed incoming attack on the
bottom modal. It works when there are multiple attacks (press multiple
times to retaliate against each consecutive attack). If the retaliation
size is smaller than the attack, the keybind can be pressed multiple
times to repeatedly blunt the incoming troop count.
Due to the current keybind logic KeyR cannot be used but Shift+KeyR can
so it is set to that until refactor.
<img width="867" height="121" alt="image"
src="https://github.com/user-attachments/assets/56e80810-4900-4db0-8ce7-1856e13529e5"
/>
No tests have been added as the retaliation feature doesn't seem to have
any to begin with since it is just an attack intent. The keybind is not
configured any differently than others. I did run all current tests and
was all good.
## 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:
n1ghtingal3
## 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:
Currently it is impossible to search for 2 letter clan tags (UN, FR,
EU), this is because of an off by one error present in the API
## 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:
Currently it is impossible to search for 2 letter clan tags (UN, FR,
EU), this is because of an off by one error present in the API
## 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:
Starting v32 maps right now!
Adds map of the Middle East. Probably one of the most highly requested
maps.
Very large map (3.4M land pixels, similar to Two Lakes, would become the
2nd or 3rd largest map by land area) of the arabian peninsula and
surrounding regions.
This map has both huge areas of land without water access full of desert
terrain, and massive trade chokepoints, which combined will result in
crazy endgames.
The theme of this map are historical, based on the end of WW1.
Historical flags have been added for the nation NPCs to use.
(High map rotation of 8 since this is probably going to become one of
the most popular maps given the relevancy of the region in real life.)
Terrain source from NASA DEM, already credited.
https://github.com/user-attachments/assets/6a1b345f-fd92-42c2-8f92-154fac4c9733
## 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:
DISCORD_USERNAME
tri.star1011
## Description:
Prevent number-key build shortcuts from opening the unit build ghost
after the player has died.
Keep build hotkeys available only while the player is alive and not in
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:
aotumuri
## Description:
Raised MAX_INTENT_SIZE from 500 to 2000 bytes — the move_warship intent
could exceed the old limit and get rejected.
Removed the separate MAX_CONFIG_INTENT_SIZE (also 2000) and the
intentType branching, since both paths now share the same cap.
## Please complete the following:
- [ ] 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:
- Inline the game worker into the main bundle (`?worker&inline`) instead
of loading it from the CDN via a same-origin Blob trampoline.
- Some users were failing to load the worker — most likely a
cross-origin / CDN edge case the trampoline didn't paper over. Inlining
serves the worker as a same-origin Blob from the main bundle, bypassing
the cross-origin `new Worker(url)` restriction entirely.
- Removes `createGameWorker()`, the trampoline script, the
`trampoline_error` message type, and the `onTrampolineError` listener in
`initialize()`. The 60s init timeout backstop stays.
- `cdnBase` is still forwarded in the `init` message — that's for
in-worker asset fetches (maps, etc.), unrelated to loading the worker
module itself.
Tradeoff: the worker bundles all of `src/core`, so the main JS download
grows by that amount on every page load (including lobby/settings pages
that never start a game). Accepted because load reliability > bundle
size here.
## 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