Commit Graph

1885 Commits

Author SHA1 Message Date
FloPinguin 4e126c2e79 Maybe for v30: Add leave confirmation dialog to JoinLobbyModal 🚪 (#3507)
## Description:

Adds a `confirmBeforeClose()` override to `JoinLobbyModal`, matching the
existing behavior in `HostLobbyModal`.

Because the german streamers had a lot of problems with accidentally
leaving today.

When a user is in a lobby and tries to close the modal (Escape key or
clicking outside), they now get a confirmation dialog asking if they
really want to leave. If the user hasn't joined a lobby yet (still on
the join form), the modal closes without prompting.

Reuses the existing `host_modal.leave_confirmation` translation key.

## 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
2026-03-24 13:26:57 -07:00
FloPinguin 0dc522413e Close private lobby when host leaves 🚪 (#3503)
## Description:

When the host of a private lobby disconnects before the game starts, the
lobby is now closed:

- **Server:** Detects host disconnection via `creatorPersistentID`
match, kicks all remaining clients with a `host_left` reason, and marks
the game as ended so it's cleaned up by the game manager and can no
longer be joined.
- **Client:** Participants receive an `alert()` saying "The host has
left the lobby." and their JoinLobbyModal is closed automatically via
the `leave-lobby` event.
- **Phase logic:** `phase()` now returns `Finished` for ended private
lobbies that haven't started, preventing the game from lingering in
`Lobby` state.

## 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
2026-03-24 13:00:20 -07:00
FloPinguin cf0cf14a1f Add new public game modifiers 🙂 (#3500)
## Description:

Adds 5 new public game modifiers to the Special game mode modifier pool:

- **Ports Disabled** - disables port construction, focus on factories
- **Nukes Disabled** - disables atom bombs, hydrogen bombs, MIRVs,
missile silos and SAM launchers
- **SAMs Disabled** - disables SAM launchers (thats funny, you cant
protect against nukes, have to space out your stuff) (mutually exclusive
with nukes disabled)
- **1M Starting Gold** - gives all players 1M starting gold (was
requested by people, new chill tier alongside existing 5M and 25M)
- **4min Peace Time** - grants 4 minutes of PVP spawn immunity

All `PublicGameModifiers` boolean fields are now optional - inactive
modifiers are omitted from game JSON instead of being serialized as
`false` (stop bloating the JSON size)

I think we have enough modifiers now :) Good variety

## 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
2026-03-24 12:57:08 -07:00
Ryan 6e67c2bf0d visibleAt (#3497)
## Description:

 needs prereq of https://github.com/openfrontio/infra/pull/272

## 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
2026-03-24 12:53:32 -07:00
FloPinguin e2d58380a7 For v30: Fix base language preferred over regional variant in auto-detection 🌐 (#3506)
## Description:

When the browser reports a locale like `de-DE`, the language selector
didn't find an exact match and fell through to candidate matching, where
it picked `de-CH` (Swiss German) over `de` (German) because longer codes
were sorted first.

This adds an early check: if the base language code (e.g. `de`) is
directly supported, return it immediately before scanning regional
candidates.

FYI @Aotumuri 

## 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
2026-03-24 12:21:13 -07:00
scamiv 2be858869c Split runtime and game logic env loading (#3505)
## 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
2026-03-24 10:54:39 -07:00
VariableVince 496f1008bb Fix: icons structure icons and others at wrong location (#3453)
## Description:

Fix for all structure icons suddenly being displaced from the actual
structure location. And in some cases, structure itself created at wrong
location, or coordinate grid, nuke trajectory preview target, nuke
circles, naval landing target, or pop-up text of gold earned from train
or tradeship.

- Was triggered on iOS almost exclusively, but was also possible on
other devices when a top/left margin was present. Like when an ad was
shown. Why noticed almost only on iOS? Because of different behaviors
where it shifts the DOM elements around relative to the screen
temporarily, so we get a top/left offset on getBoundingClientRect for
the canvas. Possible culprit is overscroll which lets you scroll outside
of the viewport for several hundred pixels before snapping back. Which
was triggered by mistake by dragging instead of tapping somewhere, or
so.

- Some of the bug reports:
https://discord.com/channels/1284581928254701718/1451393982159523982,
https://discord.com/channels/1284581928254701718/1463548362526822649,
https://discord.com/channels/1284581928254701718/1378672255336189964,
fixes https://github.com/openfrontio/OpenFrontIO/issues/3406

- The fix brings a little performance win as well because we need to be
doing less calculations. It is basically "if drawing on a canvas, work
with canvas coordinates and not with screen coordinates". Was stress
tested by two players and me, see below for reproduction.

- (BTW. When researching if the current logic was intended in any way, I
found CodeRabbit had already noticed part of the bug twice. One of them
was
[resolved](https://github.com/openfrontio/OpenFrontIO/pull/2059#discussion_r2396277710),
the other [left
open](https://github.com/openfrontio/OpenFrontIO/pull/2059#discussion_r2370413213).
Another reminder that we need to heed Rabbits calls!)

**CONTAINS**
- StructureIconsLayer > computeNewLocation and StructureDrawingUtils >
createUnitContainer. In renderLayer, when TransformHandler.hasChanged
(after onZoom, goTo, onMove), computeNewLocation recalculates the
position of all structure icons. Sometimes all icons would suddenly be
displaced, not above their map position but somewhere else. New single
icons/levels/sprites would be placed wrongly too by computeNewLocation
and createUnitContainer.
-- Fix: don't use TransformHandler > worldToScreenCoordinates but the
new worldToCanvasCoordinates. Because worldToScreenCoordinates adds the
canvas boundingRect top/left offset. When the main canvas is already
shifted down with a margin, the icons also get shifted within the
canvas. So they're moved twice as much as they should be. We only need
to know at what canvas location to put the icons, the main GameRender
canvas already handles the overall displacement.

- TransformHandler > worldToCanvasCoordinates
-- Use the new worldToCanvasCoordinates too instead of
worldToScreenCoordinates in CoordinateGridLayer, MoveIncicatorUI,
NavalTarget, NukeTeleGraph, TextIndicator. They were affected by the
same bug as the shifting Structure icons because the boundingRect
top/left offset was applied twice, but it was noticed less. I have seen
reports of NavalTarget or MobveIndicatorUI (for warships) not being in
the correct spot though iirc. And just like for
StructureIconsLayer/StructureDrawingUtils, it's only logical. Since
we're drawing on the canvas, we only need to know where to place things
within that canvas.

- TransformHandler > worldToScreenCoordinates
-- Split in two sub-functions. New seperate function
worldToCanvasCoordinates was needed for the above fix. For
canvasToScreenCoordinates the reason is explained below.

- TransformHandler > screenToWorldCoordinates: this function already
subtracts the canvas boundingRect top/left offset. Some callers were
themselves getting the boundaryRect and subtracting top/left, before
calling screenToWorldCoordinates. Not only unnecessary. But also, when
there was more than 0 top/left offset, it would be subtracted twice from
the mouse/tap position. Meaning a (ghost) structure or nuke trajectory
preview target would not be put where the mouse/tap was. Same bug as
above but reversed.
-- Checked all callers. Most did it right. Fixes where needed in
StructureIconsLayer > createStructure and renderGhost, and in
NukeTrajectoryPreviewLayer > updateTrajectoryPreview and
updateTrajectoryPath.
-- Removed comment in screenToWorldCoordinates that talked about zoom.
It doesn't belong there because we do more than zooming there, it was
probably copied once from onZoom() which has the exact same comment and
it belongs in that function.
-- Small fix in caller BuildMenu when checking all callers of
screenToWorldCoordinates: it checked if clickedCell was null, but
screenToWorldCoordinates never returns null.

- TransformHandler > added public helper functions
screenToCanvasCoordinates and canvasToScreenCoordinates to make code
re-usable
-- screenToCanvasCoordinates is used in screenToWorldCoordinates and
onZoom in TransformHandler itself
-- screenToCanvasCoordinates is now also used also in moveGhost and
createGhostStructure in StructureIconsLayer. No bugs there, but the same
calculation was done (getting boundingRect, subtracting left/top from
mouse/tap position). So they now use the centralized function which also
adds to their readability.
-- canvasToScreenCoordinates is (for now) only used in
worldToScreenCoordinates in TransformHandler. It makes the function more
readable imo. And, since it has such a similar calculation to
screenToWorldCoordinates, it only seemed neat to have them both have
their own function.

**BEFORE** (only showing "all structure icons get displaced", but the
cause and fix is basically the same for all)
https://youtu.be/CfDdUwIRQeE

**AFTER**
https://youtu.be/w5w_wvh5V0g 

## 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
2026-03-23 16:27:46 -07:00
VariableVince eb51853b05 Perf/Fix: spawn and other functions that need closest by unit (#3243)
## Description:

Performance improvements.

- **PlayerImpl**: for _nukeSpawn_, cache config to const.
- **Other files**: for nukeSpawn and other functions doing the same,
introduce findClosestBy function.
- for **TradeShipExecution**, with the move from _distSortUnit_ to
_findClosestBy_, also add check if port isActive, !_isMarkedForDeletion_
and !_isUnderConstruction_. These checks should have been there already,
so now do it in one go to make use of the predicate isCandidate in
findClosestBy.
- for **TradeShipExectution.test.ts**, add mock functions for
_isMarkedForDeletion_ and _isUnderConstruction_ because of the above.
Also, set Unit tiles and Pathfinding node to actual valid TileRefs for
the testing map. This prevents NaN as return value from manhattanDist.
This problem was already present with the use of distSortUnit, but that
function just did NaN - NaN, returned the first and only port unit in
the array and called it a day. For findClosestBy we have to make sure
the predicate manhattanDist actually returns a number instead of NaN so
we need actually valid tiles. We now have a working test instead of a
test that actually silently failed like before.
- **PlayerImpl**: _warshipSpawn_ and _nukeSpawn_: Make use of the
isCandidate predicate of findClosestBy to have warshipSpawn not return
ports under construction or (smaller change) inactive. This fixes a bug
i have seen right away (where Warship spawns from under construction
Port).
Same for _nukeSpawn_ silos, don't return inactive silo just to be sure
now that we can easily add it to isCandidate predicate anyway. This
costs no performance in the _nukeSpawn_ benchmarks actually. This should
as a by-effecft fix an edge case bug i have seen, where a nuke is sent
from a phantom silo.

Some of this goes along with PR #3220 since playerImpl buildableUnits
makes use of the underlying spawn functions via canBuild. Just like
ConstructionExecution does. But i didn't want to add more to PR 3220
since there's already a lot in there.

The new function _findClosestBy_ could also be applied to some other
parts of code to benefit of it being faster, so i did that.

_findClosestBy_ uses _findMinimumBy_, which is a little more generic in
name. I think _findMinimumBy_ could be used by other parts of code,
while _findClosestBy_ is more clear naming for what it does now. But we
could ditch _findMinimumBy_ and just leave findClosestBy?

Examples of synthetic benchmarks (not included in this PR):

**BEFORE CHANGES (before Scamiv's PR #3241)**
<img width="705" height="91" alt="image"
src="https://github.com/user-attachments/assets/d6d91c08-39f1-4387-9ccc-e51951caa539"
/>

<img width="751" height="101" alt="image"
src="https://github.com/user-attachments/assets/80d400ac-3408-4107-aa58-6d2a847311e9"
/>

**AFTER CHANGES (before  Scamiv's PR #3241)**
![findunittoupgrade for loop 5th
run](https://github.com/user-attachments/assets/b840111b-e7e0-49b5-ace1-299a322224b5)

![nukespawn for loop 3rd
run](https://github.com/user-attachments/assets/47cfc444-9549-4887-8c0e-007277d24485)


**BEFORE CHANGES (after Scamiv's PR #3241)**
![findunittoupgrade
BEFORE](https://github.com/user-attachments/assets/c51e2cec-6171-4204-ba3f-48ed282978eb)

![nukespawn
BEFORE](https://github.com/user-attachments/assets/f7ce9a33-32d6-4875-a529-41724fd4d89f)

**AFTER CHANGES (after Scamiv's PR #3241)**
<img width="717" height="96" alt="image"
src="https://github.com/user-attachments/assets/5b106843-bf6e-4448-a8e8-94448fb30ced"
/>

<img width="767" height="92" alt="image"
src="https://github.com/user-attachments/assets/e6714c7b-26c1-455b-adae-f0060f1cbc7b"
/>




_Also see more **BEFORE** and **AFTER** in this comment:_

https://github.com/openfrontio/OpenFrontIO/pull/3243#issuecomment-3949060395

_And here a comparison in the flame charts:_

- based on the same replay and tried to get the performance recording
going at the same speed and length but always end up with small
differences
- because of a bug in replays currently, it puts you in with the same
clientID/persistantID currently. This means we can also record part of
what is normally only recordable with live human input (the
playerActions/playerBuildables).


**BEFORE** flame chart with nukeSpawn (human player) and maybeSendNuke
(Nation players, uses nukeSpawn via canBuild):

![BEFORE nukespawn Schermafbeelding 2026-03-04
231707](https://github.com/user-attachments/assets/3de7de16-769e-4748-b201-d71c5b75e16e)

![BEFORE maybesendnuke B Schermafbeelding 2026-03-04
230009](https://github.com/user-attachments/assets/16924c77-21c2-4a2d-b784-a469dce15538)

![BEFORE main build Schermafbeelding 2026-03-04
222017](https://github.com/user-attachments/assets/67e99fd6-335c-4e12-a9dc-ad5ae7d74de4)


**AFTER** flame chart with nukeSpawn (human player) and maybeSendNuke
(Nation players, uses nukeSpawn via canBuild):

![AFTER nukespawn Schermafbeelding 2026-03-04
230613](https://github.com/user-attachments/assets/a4eec0ae-d654-44c9-bf89-61567203d748)

![AFTER maybesendnuke B Schermafbeelding 2026-03-04
230009](https://github.com/user-attachments/assets/80e2366d-406b-403a-854c-6fa156713abc)

![AFTER maybesendnuke C Schermafbeelding 2026-03-04
230009](https://github.com/user-attachments/assets/71497e8a-81d0-4722-80f7-427f09d9c21e)

![AFTER maybesendnuke D Schermafbeelding 2026-03-04
230009](https://github.com/user-attachments/assets/55f131cc-e6e5-48f2-9e8d-771c60280640)

![AFTER main build Schermafbeelding 2026-03-04
222017](https://github.com/user-attachments/assets/1927ecb6-d54d-4e1e-8aa4-4f97602e2234)


## 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
2026-03-23 16:23:49 -07:00
evanpelle ef846c895b make ffa the large game card 2026-03-23 13:58:49 -07:00
scamiv 05e2bc9f0a Improve cacheability with content-hashed public assets and a cacheable app shell (#3494)
## 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
2026-03-23 11:36:52 -07:00
Mattia Migliorini e3a14671ab Bring dev pattern back (#3495)
## Description:

Dev pattern support was removed in
97d0a05d58
This PR brings it back to allow testing new skins.

## 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:

deshack_82603
2026-03-23 10:01:19 -07:00
FloPinguin bf09b9c9be Improve JoinLobbyModal (#3482)
## Description:

Perviously, JoinLobbyModal did not show settings like "Infinite gold" or
"Instant Build" or changed tribe count.
Now it does. Only if the setting differs from the default. I tested a
lot of scenarios, I also thought of the public game modifiers.
And we show a small map image now.

Public game with lots of modifiers:

<img width="780" height="758" alt="Screenshot 2026-03-21 011805"
src="https://github.com/user-attachments/assets/9d3fcaa9-3a50-42b2-a351-ac737ef18230"
/>

A private game with lots of custom settings:

<img width="776" height="530" alt="Screenshot 2026-03-21 011940"
src="https://github.com/user-attachments/assets/8f9a3809-844d-4f24-8f92-46c4ce480f8c"
/>

A private game with disabled units:

<img width="786" height="562" alt="Screenshot 2026-03-21 012134"
src="https://github.com/user-attachments/assets/61058329-1d86-4667-a945-7819b89cbf41"
/>

Regular public FFA (No modifiers):

<img width="780" height="372" alt="Screenshot 2026-03-21 012228"
src="https://github.com/user-attachments/assets/abdc42f0-8f2c-40c1-8719-76c648a12bae"
/>

This PR also includes a fix for UsernameInput:

<img width="910" height="647" alt="Screenshot 2026-03-20 222021"
src="https://github.com/user-attachments/assets/e1922395-9dfc-4b32-b987-e2dbff9af917"
/>

This PR also fixes the default private lobby difficulty in GameManager

## 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
2026-03-20 20:43:35 -07:00
VariableVince 13df5cf324 Perf/Cleanup/Fix(NameLayer): 40% better performance (#3475)
## Description:

TL;DR: NameLayer cleanup+ fix + about 40% faster. The potential move of
NameLayer to canvas is stalled so this is a welcome improvement until
then imo.

- It was previously attempted to move NameLayer from HTML to canvas. But
currently that work is stalled so it might take awhile. Therefore
optimizations to NameLayer are useful to merge in the meantime. Also
there's a fix in this PR (see point below) and some cleanup. Overall it
would probably be better to base future changes on this better version
of NameLayer.
Messages about attempt on Feb 6 and reference to having done that
attempt on March 3:

https://discord.com/channels/1359946986937258015/1381293863712591872/1469117172767784981

https://discord.com/channels/1359946986937258015/1381293863712591872/1469401090385641573

https://discord.com/channels/1359946986937258015/1381293863712591872/1469435973522686127

https://discord.com/channels/1359946986937258015/1359946989046989063/1478213329079242752

- Fix: TL;DR: Remove redundant comparison that unintentionally didn't
work and always resolved to true. Leading to scale always being
recalculated. It is now still always recalculated because otherwise name
may be too big for the territory for several seconds, which looks buggy.
(More on this: In renderPlayerInfo(), it cached render.location in
oldLocation. Then put new Cell() in render.location. Later on it would
strictly compare render.location against oldLocation, to decide if scale
should be changed. Which would always be true because render.location
would have a new Object (long ago they were compared non-strictly with
==, later on strictly when those checks were updated in the entire repo
to ===). With this comparison always returning true (even if
render.location x and y did not actually change), the scale would always
be updated by updating render.element.style.transform.
After the fix (removing the non-working comparison which always resolved
to true), scale updates happen at same frequency as before. I have not
kept a similar check like "positionChanged". Because in testing,
player/tribe name would be scaled as too big for their territory size
for several seconds. This felt buggy. Cause for this is two delays
sometimes overlap resulting in several seconds of delay before scale is
recalculated after name position changed: time in Namelayer per render
refresh inside renderLayer (renderRefreshRate 500ms) plus the waiting
time in PlayerExecution per recalculation of largestClusterBoundingBox
(every 20 ticks). I ultimately decided that it should not wait for
"positionChanged" and just be updated every 500ms (renderRefreshRate)
just like unintentionally happened before.)

- Remove redundantly re-adding the name, when a player name doesn't
change anyway. Only adding it when creating the element is enough
- Remove dead code for Shield
- Cache DOM lookups
- Use textContext instead of innerHTML for nameSpan
- Only transform container if it has updates
- Remove currently unused Canvas. Also from public renderLayer().
Layer.ts expects renderLayer(context: CanvasRenderingContext2) so i
could put it back, but it isn't needed per se and i think it makes more
clear that NameLayer doesn't use Canvas currently.
- Remove two unneeded/outdated comments, update others
- Move setting render.fontSize and render.fontColor after early return
- Pass baseSize to updateElementVisibility so as to not calculate it
twice
- Cache render.player.nameLocation() to re-use

BEFORE:
![BEFORE 610 PsvZSjuL for 20 2s Schermafbeelding 2026-03-13
003100](https://github.com/user-attachments/assets/33583004-8a01-4587-9908-dafd0816f2b4)

AFTER:
![AFTER 610 PsvZSjuL for 20 2s Schermafbeelding 2026-03-13
003100](https://github.com/user-attachments/assets/a8c70ef6-c16a-4204-82b3-d916ba3d6437)

## 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
2026-03-20 15:34:19 -07:00
VariableVince c29fca477b Fix(HelpModal): small updates and fixes (#3473)
## Description:

- fix info icon spacing
- update multiple texts to reflect current state, rewrote
"ui_playeroverlay_desc" further for better readability
- add text for the options menu, and change their order to reflect
current button order
- add missing "Stop trading" icon, is PNG so lazy load
- remove uneccesary lazy loading for an SVG icon (rest of the SVGs
aren't lazy loaded either)

Didn't touch the rest although more incremental updates are needed
following UI and other changes.

Before:
<img width="242" height="82" alt="image"
src="https://github.com/user-attachments/assets/8f38eef6-21e7-4b18-84ef-adc4161a317f"
/>

<img width="357" height="167" alt="image"
src="https://github.com/user-attachments/assets/c6937b5c-c1b2-4560-b40b-94b24a4906cc"
/>

After:
<img width="262" height="95" alt="image"
src="https://github.com/user-attachments/assets/15c1e9f5-3e27-4f4b-8472-5bb70234ab42"
/>

<img width="345" height="203" alt="image"
src="https://github.com/user-attachments/assets/3d3fe3c5-98b2-41fb-8f79-48d02d7ecf9b"
/>

## 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: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-20 14:03:06 -07:00
Mattia Migliorini 5562993042 Highlight own player name and team in lobby modal (#3468)
## Description:

> I always lose track of myself in lobbies, could our names be bolded or
even our entire team bolded for ease of readability?

This PR highlights own username and own team in lobby modal, to make it
easy for players to track themselves.
It's also useful when your username gets changed by the profanity
filter.

Based on [this Discord
suggestion](https://discord.com/channels/1284581928254701718/1484056268355145778)

### Screenshots

FFA (Public):

<img width="916" height="805" alt="image"
src="https://github.com/user-attachments/assets/5d769b48-3217-495c-bdcd-9a61fd05cd8f"
/>

Teams (Public):

<img width="912" height="798" alt="image"
src="https://github.com/user-attachments/assets/0b3966f9-4094-4dfe-a11f-ce225824690a"
/>

FFA (Private):

<img width="912" height="798" alt="image"
src="https://github.com/user-attachments/assets/21ac32ac-1fd0-4318-b2c8-44f3a2756e2e"
/>

<img width="906" height="800" alt="image"
src="https://github.com/user-attachments/assets/7530c4a2-3bc7-4b24-828d-766d65b85f89"
/>

Teams (Private):

<img width="907" height="799" alt="image"
src="https://github.com/user-attachments/assets/c8773fb8-5015-4247-8fde-4628f0850660"
/>

<img width="910" height="800" alt="image"
src="https://github.com/user-attachments/assets/15c56a54-ffea-45ef-a86f-1a8be6995e1d"
/>


## 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:

deshack_82603
2026-03-19 15:58:06 -07:00
Ralfi Salhon 015e3c7d19 feat: Attacking Troops Overlay (#3427)
## Description:

https://troop-advantage-layer.openfront.dev/

Hey OpenFront dev team, I've been really enjoying the game, and the
v0.30 changes have felt great so far. Happy to start contributing!

This PR introduces `AttackingTroopsOverlay`, a layer that renders live
attacker vs. defender troop counts directly on active front lines.
Players can immediately gauge combat strength without leaving the map
view.


![troop-advantage-layer](https://github.com/user-attachments/assets/9e862812-84b4-46cb-a0c6-65fa50320198)

A recent change updates the layer to just the # of attackers and a
symbol for attack/defence:

![visual-front-line](https://github.com/user-attachments/assets/46bc7117-2314-44c9-96fc-8a7e9c6ab5cd)

Left: Perspective of Anon 667 (Blue) | Right: Perspective of Anon332
(Red)

![ezgif-6261e6669d6b972b](https://github.com/user-attachments/assets/734d90c1-8f22-44dc-8f2f-b22e46676f46)

**How it works:**
- Attacker count shown for ground invasions. When attacking, your troop
count will display amber for disadvantageous, and green for advantageous
battles. When defending, the enemy troop count will switch to red if you
are at a severe disadvantage.
- Label position recalculates every tick at 200ms, tracking the front
line as it moves.
- Automatically hidden during Terrain view (spacebar)
- Labels clean up when an attack ends or its target becomes invalid

**Settings:** An "Attacking Troops Overlay" toggle is added to Settings,
enabled by default.
--> the screenshot is old, but the text has been updated
<img width="448" height="410" alt="Settings toggle"
src="https://github.com/user-attachments/assets/2df8ec7a-3f77-48b7-a9b5-ee4a6eed0412"
/>

## Checklist

- [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

## Discord
Radyus
2026-03-19 15:04:33 -07:00
evanpelle 6b9603d445 Merge branch 'v30' 2026-03-18 19:29:42 -07:00
evanpelle c5dad1f850 on purchase redirect, check cosmetic param instead of pattern param 2026-03-18 19:27:33 -07:00
Evan 3e7088cb2a Close join error (#3454)
## Description:

When joining a game after it fills up, the server rejects the player
join and the player leaves the lobby, but the join modal stays up.

## 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
2026-03-17 19:20:13 -07:00
Evan cb7164a52f Place footer ad during spawn phase (#3458)
## Description:

Show the "footer_ad" ad type during the spawn phase.

<img width="1855" height="914" alt="Screenshot 2026-03-17 at 7 01 37 PM"
src="https://github.com/user-attachments/assets/9c4c1730-95a0-4fc7-a983-4fe625d38c80"
/>

## 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
2026-03-17 19:02:55 -07:00
Ryan 1049b7e7dc Clan System Part 1 (#3276)
## Description:

Properly split out clantags and usernames, a clantag should not be part
of a username.

<img width="285" height="286" alt="image"
src="https://github.com/user-attachments/assets/8ac56e82-b12c-4fc0-9774-e445252a6e61"
/>

https://api.openfront.dev/game/ojkqZFb2


<img width="296" height="596" alt="image"
src="https://github.com/user-attachments/assets/85152f80-c111-4f87-b85b-8516c9c6137b"
/>


https://api.openfront.dev/game/MF32BkVc


requires;
https://github.com/openfrontio/infra/pull/264

## 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
2026-03-17 15:55:47 -07:00
FloPinguin a30859d132 Fix labels in public lobby modifier display 🏷️ (#3456)
## Description:

Fixes two issues in the join lobby modal's modifier display:

1. **Starting Gold**: Label showed "Starting Gold (Millions)" with value
"5M", duplicating "millions". Now shows "Starting Gold" as the label,
keeping "5M" as the value.
2. **Disable Alliances**: Label showed "Disable Alliances" with value
"Enabled", which is confusing. Now shows "Alliances" as the label with
"Disabled" as the value.

Join Lobby Modal needs a general rework, I will probably make an PR

## 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
2026-03-17 15:31:11 -07:00
FloPinguin 9785666b98 Fix labels in public lobby modifier display 🏷️ (#3456)
## Description:

Fixes two issues in the join lobby modal's modifier display:

1. **Starting Gold**: Label showed "Starting Gold (Millions)" with value
"5M", duplicating "millions". Now shows "Starting Gold" as the label,
keeping "5M" as the value.
2. **Disable Alliances**: Label showed "Disable Alliances" with value
"Enabled", which is confusing. Now shows "Alliances" as the label with
"Disabled" as the value.

Join Lobby Modal needs a general rework, I will probably make an PR

## 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
2026-03-17 15:30:52 -07:00
VariableVince 9f8a2d2d84 Fix "you didn't enter the lobby in time" when device clock isn't synced (#3451)
## Description:

If the time on the local device differs from the server time, users may
see the message “You did not join the lobby on time.”

Resolve this by accounting for the time difference, reusing the logic in
`JoinLobbyModal` that was previously in `GameModeSelector`, and
centralizing it into `ServerTime.ts`.

Bug reports:
https://github.com/openfrontio/OpenFrontIO/issues/3428

https://discord.com/channels/1284581928254701718/1482511096597315815

https://discord.com/channels/1284581928254701718/1482382264011591781

Resolves #3428 

## 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
2026-03-17 15:21:38 -07:00
VariableVince 82d0fb385d Fix "you didn't enter the lobby in time" when device clock isn't synced (#3451)
## Description:

If the time on the local device differs from the server time, users may
see the message “You did not join the lobby on time.”

Resolve this by accounting for the time difference, reusing the logic in
`JoinLobbyModal` that was previously in `GameModeSelector`, and
centralizing it into `ServerTime.ts`.

Bug reports:
https://github.com/openfrontio/OpenFrontIO/issues/3428

https://discord.com/channels/1284581928254701718/1482511096597315815

https://discord.com/channels/1284581928254701718/1482382264011591781

Resolves #3428 

## 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
2026-03-17 13:52:35 -07:00
evanpelle 79c3deabd8 remove spawn video ad 2026-03-17 11:40:42 -07:00
Evan cce46ef126 Refactor: use promises instead of callbacks for joining a game (#3452)
## Description:

Simplifies the interface a bit.

## 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
2026-03-17 10:08:30 -07:00
Evan 51db6a2772 Add in game add to bottom left corner (#3446)
## Description:

Remove the header ad because that's where the player info overlay is,
and instead put it on the bottom left

Ad is not displayed on small screens.

<img width="1916" height="924" alt="Screenshot 2026-03-16 at 7 27 58 PM"
src="https://github.com/user-attachments/assets/e39c4be6-5270-4a77-bfbd-9521d15b9211"
/>

## 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
2026-03-16 19:36:02 -07:00
bijx f356f09f81 Feat: Game Speed + Pause keybinds (#3397)
## Description:

Can't tell you how many times I've been playing solo, I try to go change
the speed from `Max` to `x1` and before I've opened the speed controls
and clicked on one the AI completely wipes me. But not to worry, we now
have a pause and speed toggle up/down keybinds!


https://github.com/user-attachments/assets/48692c27-888f-40fb-837a-45e26f262441

Keybinds were added to "Menu Shortcuts" submenu:

<img width="1750" height="1099" alt="image"
src="https://github.com/user-attachments/assets/8c4500d5-f43e-4a1c-9940-04db75bf18cf"
/>

Tested on Solo, custom match, and public lobbies. Pause intent works
correctly on solo and private match (only if you are host), and neither
feature works in public matches, as expected.



## 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:

bijx
2026-03-16 23:16:59 +00:00
HulKiora b045608c89 ui: reduce HUD transparency for control, attacks, events, and hover panel (#3429)
## Description:

Reduce HUD transparency for better readability by making the Control
Panel, Attacks panel, Events/Chat panel, and Hover panel more opaque
while keeping a subtle blur effect.

## Please complete the following:

- [ x] 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:

HulKiora
Before :
<img width="518" height="134" alt="image"
src="https://github.com/user-attachments/assets/dd714a8e-5b8c-4754-b68d-a86a0f82afcf"
/>

After :
<img width="519" height="138" alt="image"
src="https://github.com/user-attachments/assets/828b88be-a071-474c-a4a2-80d1c679b3df"
/>
2026-03-15 14:59:34 -07:00
evanpelle 3919f17e8b Revert "Rearrange homepage game boxes & tune special modifier probabilities 🎲 (#3420)"
This reverts commit ea4355f03a.
2026-03-14 19:29:57 -07:00
evanpelle c8ed6b0e70 Merge branch 'v30' 2026-03-14 19:28:38 -07:00
Josh Harris 4b33f3749d Fix duplicate game creation when opening private lobby (#3423)
## Description:

- Prevent `BaseModal.open()` from firing `onOpen()` twice when
Navigation.showPage() re-calls `open()` on inline modals
- Adds an `isModalOpen` early-return guard, matching existing behavior
in `OModal.open()`

## 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:

Jish
2026-03-13 19:34:59 -07:00
evanpelle f2a7493bbf Revert "temporarily disable homepage footer ad"
This reverts commit 3ab778ace4.
2026-03-13 12:31:33 -07:00
evanpelle add21ace42 Revert "Rearrange homepage game boxes & tune special modifier probabilities 🎲 (#3420)"
This reverts commit ea4355f03a.
2026-03-13 12:14:36 -07:00
evanpelle 536d3e63ae don't render ranked button on crazy games 2026-03-13 12:01:11 -07:00
FloPinguin ea4355f03a Rearrange homepage game boxes & tune special modifier probabilities 🎲 (#3420)
## Description:

**Homepage layout:** Reorder the game mode cards so FFA is the left
(large) box, Teams is the upper-right box, and Special Games is the
lower-right box. Mobile order updated to match (FFA → Teams → Special).

**Special game modifiers:**
- Adjusted modifier count roll to 40%/40%/15%/5% for 1/2/3/4 modifiers
(was 30%/40%/20%/10%) because having so many special games with 3/4
modifiers while we only have 8 modifiers in the pool is a bit dumb (from
the 8 modifiers two are mutually exclusive and 4 should be quite rare).
- Changed ticket counts in `SPECIAL_MODIFIER_POOL` so
isAlliancesDisabled and isHardNations are more rare.

## 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
2026-03-13 11:55:03 -07:00
FloPinguin 2419f77f17 Rearrange homepage game boxes & tune special modifier probabilities 🎲 (#3420)
## Description:

**Homepage layout:** Reorder the game mode cards so FFA is the left
(large) box, Teams is the upper-right box, and Special Games is the
lower-right box. Mobile order updated to match (FFA → Teams → Special).

**Special game modifiers:**
- Adjusted modifier count roll to 40%/40%/15%/5% for 1/2/3/4 modifiers
(was 30%/40%/20%/10%) because having so many special games with 3/4
modifiers while we only have 8 modifiers in the pool is a bit dumb (from
the 8 modifiers two are mutually exclusive and 4 should be quite rare).
- Changed ticket counts in `SPECIAL_MODIFIER_POOL` so
isAlliancesDisabled and isHardNations are more rare.

## 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
2026-03-13 11:54:39 -07:00
evanpelle 681aa98fb1 bugfix: don't remove player name renderer if name location is not set 2026-03-13 11:53:38 -07:00
evanpelle 3ab778ace4 temporarily disable homepage footer ad 2026-03-13 11:45:12 -07:00
evanpelle 28b2372ef5 shorten the control panel vertically a bit to increase center space for gameplay 2026-03-13 09:46:47 -07:00
Evan 37f4f606f6 reduce margin for player info & control panel (#3414)
## Description:

To give more space in the center of the screen remove the top & bottom
margin. Also make the control panel & player info panel slightly shorter
& longer.

<img width="1167" height="961" alt="Screenshot 2026-03-12 at 7 20 40 PM"
src="https://github.com/user-attachments/assets/15baf640-daf0-4394-9107-d175130e7754"
/>

## 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
2026-03-12 19:50:43 -07:00
evanpelle 36572b69b5 radial menu: make elements brighter on hover 2026-03-11 20:35:23 -07:00
FloPinguin 5e84227b54 Fix: Lobby websocket reconnects after stop() in singleplayer 🔧 (#3407)
## Description:

Calling `lobbySocket.stop()` closes the WebSocket, but the async `close`
event fires afterward and triggers `handleClose()` →
`scheduleReconnect()`, reopening the connection. In singleplayer, both
`onPrestart` and `onJoin` callbacks fire synchronously in a single
stack, so the second `stop()` call has nothing left to clean up - the
reconnect timeout gets set after both calls complete.

Adds a `stopped` flag that skips reconnection in `handleClose()` when
the socket was intentionally closed.

Surely this PR description is not AI generated.

Should reduce network load of the server because all the singleplayer
gamers no longer stay connected to the lobbies websocket forever. They
also no longer load all the map mainfest.jsons and thumbnails while they
are playing alone (rotation running in the background).

- [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
2026-03-11 14:36:32 -07:00
Evan e922d336a1 update radial menu styling (#3404)
## Description:

Before:

<img width="215" height="239" alt="Screenshot 2026-03-10 at 10 29 12 PM"
src="https://github.com/user-attachments/assets/bb044425-eb2f-427c-afd6-6c9dd5d075aa"
/>

After:

<img width="240" height="207" alt="Screenshot 2026-03-10 at 10 27 33 PM"
src="https://github.com/user-attachments/assets/21ce4c3b-ab24-4af0-b608-6be5603320fb"
/>

## 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
2026-03-11 10:06:38 -07:00
FloPinguin 8329dd479e Mobile UI polish (#3401)
## Description:

### Fix attack percentage label causing slider width jitter on mobile by
setting a fixed width on the container


https://github.com/user-attachments/assets/41ea3b98-48fa-49f3-b22e-2a567f208dfb

### Reduce event panel max-height from 30vh to 15vh on mobile

Previous:

<img width="406" height="781" alt="Screenshot 2026-03-10 231050"
src="https://github.com/user-attachments/assets/ea83a75d-a0cb-4ce2-8714-99e822b028e9"
/>

After:

<img width="397" height="781" alt="Screenshot 2026-03-10 231414"
src="https://github.com/user-attachments/assets/153a4291-a3dc-4255-8a19-725bfdf6ff8b"
/>

### Change lobby buttons (Create/Ranked/Join) from `slate-700` to
`slate-600` for better contrast against the background

Previous:

<img width="411" height="387" alt="Screenshot 2026-03-10 232720"
src="https://github.com/user-attachments/assets/1fd8bc7a-dbaa-414a-8e74-a070c66c40c6"
/>

After:

<img width="408" height="495" alt="Screenshot 2026-03-10 232704"
src="https://github.com/user-attachments/assets/e7693a28-6560-4440-8581-91b1fd7a5cf5"
/>

## 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
2026-03-10 16:40:15 -07:00
Evan 3a195005d8 outgoing attacks (#3398)
## Description:

Show outgoing attacks on the PlayerInfoPanel. Also update the
AttacksDisplay so it uses the same icon for incoming & outgoing attacks

<img width="502" height="97" alt="Screenshot 2026-03-10 at 2 02 49 PM"
src="https://github.com/user-attachments/assets/de83bf4d-b1b9-4fec-a8c6-e25bbf82c942"
/>

<img width="483" height="167" alt="Screenshot 2026-03-10 at 2 03 02 PM"
src="https://github.com/user-attachments/assets/cf43106f-5f3a-4a78-abae-dff019de4fb4"
/>

<img width="509" height="203" alt="Screenshot 2026-03-10 at 2 03 28 PM"
src="https://github.com/user-attachments/assets/79db2081-dd93-4ff4-aaa7-75281c16c037"
/>


## 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
2026-03-10 14:08:31 -07:00
bijx 8eb879aff4 Fix: Coordinate Grid text is now white when dark mode is enabled (#3396)
## Description:

When the game was on dark mode, seeing the coordinate text on oceans
became impossible. Now we check for dark mode in the layer to change the
text color accordingly.

Light Mode:
<img width="3430" height="1669" alt="image"
src="https://github.com/user-attachments/assets/181d53f5-b218-4f85-9d06-7e80f5d20004"
/>

Dark Mode: 
<img width="3488" height="1706" alt="image"
src="https://github.com/user-attachments/assets/8a8e3951-31b4-448d-bc87-7fabd105e5fb"
/>


## 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:

bijx
2026-03-10 19:59:08 +00:00
Evan f61d2fb59f update control panel UI: larger text, troop colors (#3395)
## Description:

During the playtests people mentioned the UI was too small. Also change
the troop color because white & green was difficult to read.


<img width="559" height="196" alt="Screenshot 2026-03-10 at 12 42 58 PM"
src="https://github.com/user-attachments/assets/9ba5ccaa-3113-4998-a016-e9936269f5de"
/>

<img width="491" height="96" alt="Screenshot 2026-03-10 at 12 43 15 PM"
src="https://github.com/user-attachments/assets/54f7886f-b830-451e-9a7a-3a764efdfbcc"
/>

<img width="574" height="208" alt="Screenshot 2026-03-10 at 12 43 25 PM"
src="https://github.com/user-attachments/assets/2583b492-1844-4814-8fd8-b7216a1dad60"
/>

## 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
2026-03-10 12:59:53 -07:00
Bartosz Woźniak 13a965a9c5 Add hotkey descriptions for Escape and Enter actions in HelpModal (#3382)
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #3064

## Description:

Adds Esc and Enter to the Help modal hotkeys section with consistent key
styling and descriptions:

- **Esc** – Shown with label; description: "Closes menu. Cancels unit
build preview."
- **Enter (↵)** – Shown with symbol and label; description: "Confirms
input. Confirms unit build preview."

Both keys use the same keycap style as existing hotkeys. Enter label
uses the standard symbol (↵) in `getKeyLabel()`. New strings are in
`en.json` under `help_modal.action_esc` and `help_modal.action_enter`.

<img width="723" height="427" alt="image"
src="https://github.com/user-attachments/assets/52fc168a-a850-4ac5-ba82-72f7672f476c"
/>


## 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:

.wozniakpl
2026-03-09 21:15:13 -07:00