## Description:
Adds Mediterranean sea map, from Iberia to Asia. Map contains ancient
Roman Empire provinces and its rivals as Nations.
This map was requested by the dev.
elevation data from Opentopography
<img width="2850" height="1450" alt="image"
src="https://github.com/user-attachments/assets/6aa5ba12-f4f7-414d-a712-b90323f1d796"
/>
<img width="590" height="304" alt="Captura de pantalla 2026-03-27
010038"
src="https://github.com/user-attachments/assets/efd1deea-bd88-4ae2-92a0-47a6626a0c0f"
/>
<img width="585" height="302" alt="Captura de pantalla 2026-03-27
005758"
src="https://github.com/user-attachments/assets/a127696e-fe34-424c-a88d-b86b99a5f414"
/>
## 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:
Update the schema for achievements
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
evan
## Description:
Adds Milky Way galaxy map based on real reconstruction by NASA. Star
density simulated as terrain. Best played in Dark Mode.
Also adds credits to JPL NASA
<img width="532" height="533" alt="Captura de pantalla 2026-03-26
142938"
src="https://github.com/user-attachments/assets/87bb19bb-4e2d-4383-a3e9-6e14b714b84c"
/>
## 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:
https://github.com/openfrontio/infra/pull/279 to go with this, splits
out 1v1
## 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:
https://github.com/openfrontio/infra/pull/279 to go with this, splits
out 1v1
## 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:
So artists get credit for the work they do.
## 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 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
## 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
## Description:
This refactors client configuration loading to make the environment
split explicit.
The app currently has two different env concerns:
- the browser main thread needs the live runtime env to select API /
Turnstile / JWT settings
- the worker and game-logic path need a build-time env to select game
config behavior
Before this change, both responsibilities were hidden behind the same
loader, which made the intent unclear and caused confusion around the
worker fallback behavior.
This PR separates those paths explicitly:
- main-thread browser code now uses `getRuntimeClientServerConfig()`
- game creation and worker/game-logic code now uses
`getGameLogicConfig()`
- the build-time game-logic env is represented explicitly as
`GameLogicEnv`
## What Changed
- Added `GameLogicEnv` to model the build-time game config choice
explicitly.
- Added `getRuntimeClientServerConfig()` for live runtime browser config
from `window.BOOTSTRAP_CONFIG`.
- Added `getBuildTimeGameLogicEnv()` and
`getServerConfigForGameLogicEnv()` for build-time worker/game-logic
config.
- Renamed game config loading from `getConfig()` to
`getGameLogicConfig()` to reflect what it actually does.
- Updated browser call sites to use the runtime client config loader.
- Updated worker/game creation paths to use the game-logic config
loader.
- Updated config loader tests to cover both paths.
## Behavior
This keeps the current intended behavior, but makes it explicit:
- Runtime client env:
- comes from `window.BOOTSTRAP_CONFIG`
- controls live browser integration settings such as API origin,
Turnstile, and JWT audience/issuer
- Build-time game-logic env:
- comes from bundled `process.env.GAME_ENV`
- maps:
- `dev` -> dev game config
- `staging` -> default/prod game config
- `prod` -> default/prod game config
That means preprod/staging deployments can continue using prod game
logic while still using staging API/auth settings on the main thread.
## Why
The previous setup worked, but the naming and loader boundaries were
misleading:
- the same function was used for both runtime browser config and
worker/game config
- the worker fallback looked like an implementation detail instead of an
intentional architectural split
This change makes that intent visible in code without changing the
desired deployment behavior.
## Please complete the following:
- [ ] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] I have added relevant tests to the test directory
- [ ] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
DISCORD_USERNAME
## Description:
Add support for purchasable/gated flags.
* Create a new "Store" modal that renders both skins & flags
* move all store related logic out of TerritoryPatternsModal
* use nation:code for existing nation flags & flag:key for gated flags
* check if user has the appropriate flags before purchasing
## 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:
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)**


**BEFORE CHANGES (after Scamiv's PR #3241)**


**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):



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





## 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
## Description:
This reworks asset delivery and cacheability across the app and moves
non-bundled public resources onto immutable, content-hashed URLs.
Vite bundle outputs continue to live under `/assets/**` and remain
content-hashed by Vite. Public resources that were previously fetched
from stable paths in `resources/` now go through a custom hashed
namespace under `/_assets/**`, backed by a generated asset manifest that
is available to the server, browser, and worker runtime.
In parallel, the root app shell is now cacheable shared HTML instead of
request-time `no-store` HTML. Dynamic and live routes remain explicitly
uncached.
## Why
- Improve browser and Cloudflare cacheability for static assets.
- Remove query-string and release-version cache busting for
runtime-fetched assets.
- Allow unchanged public assets to keep the same URL across releases.
- Reduce avoidable work on `/` by serving a shared app shell instead of
rendering HTML on every request.
- Make cache behavior explicit instead of relying on mixed framework
defaults and file-extension heuristics.
## What Changed
### 1. Content-hashed public asset pipeline
- Added a build-time public asset manifest and hashing pipeline for
non-Vite resources.
- Production now emits hashed public assets under `/_assets/**`.
- Added runtime manifest loading for Node so server-rendered paths
resolve against built hashed files instead of rebuilding from source at
runtime.
- Emitted the runtime asset manifest as an ESM module for server
consumption.
Result:
- `/assets/**` = Vite-managed hashed bundle outputs
- `/_assets/**` = custom content-hashed public resources
### 2. Runtime asset URL migration
- Added a shared `assetUrl(...)` resolution path.
- Migrated runtime references away from query-string versioning and
stable source paths.
- Updated browser, worker, and server-side rendering paths to resolve
through the asset manifest.
- Moved map manifests, map binaries, thumbnails, sprites, sounds, fonts,
flags, icons, screenshots, and other runtime-fetched resources onto
hashed URLs.
### 3. Map and preview fixes
- Fixed directory and per-file map asset resolution so map manifest and
binary fetches resolve to the correct hashed URLs.
- Updated preview metadata and map thumbnail paths to use the hashed
asset namespace.
- Fixed runtime manifest loading in prod after deployment.
### 4. Explicit cache policies
- Added explicit immutable cache headers for:
- `/assets/**`
- `/_assets/**`
- worker-prefixed equivalents under `/wN/...`
- Added explicit `no-store` headers for live and dynamic APIs.
- Removed the old `/api/env` bootstrap request and baked `gameEnv` into
the HTML bootstrap instead.
### 5. Cacheable root app shell
- Refactored the root HTML path to serve a shared app shell with:
- `Cache-Control: public, max-age=0, s-maxage=300,
stale-while-revalidate=86400`
- `/` and the SPA fallback now serve shared cacheable HTML instead of
request-time `no-store` rendering.
- `/game/:id` remains dynamic and `no-store`, but now reuses the shared
shell before injecting preview tags.
### 6. Matchmaking instance handling
- Because the app shell is now cacheable, `INSTANCE_ID` was removed from
shared HTML.
- Added `/api/instance` as a temporary `no-store` runtime lookup used
only by matchmaking.
- This preserves correctness with the current random-per-boot
`INSTANCE_ID` model while keeping `/` cacheable, but it is not the
intended long-term design.
## Behavior Changes
### Asset URL contract
Production URLs for non-Vite public resources now change from stable
paths such as:
- `/maps/...`
- `/images/...`
- `/manifest.json`
to content-hashed paths under:
- `/_assets/...`
Examples:
- `/_assets/maps/<map>/manifest.<hash>.json`
- `/_assets/images/Favicon.<hash>.svg`
### Bootstrap/config
- `/api/env` is removed.
- `gameEnv` is now bootstrapped from HTML.
### HTML caching
- `/` and the SPA fallback are now cacheable shared HTML.
- `/game/:id` remains dynamic.
## Cache Matrix After This Branch
- `/_assets/**`: `public, max-age=31536000, immutable`
- `/assets/**`: `public, max-age=31536000, immutable`
- live `/api/**`: explicit `no-store`
- `/api/health`: explicit `no-store`
- `/api/instance`: explicit `no-store`
- `/game/:id`: explicit `no-store`
- `/` and SPA fallback: `public, max-age=0, s-maxage=300,
stale-while-revalidate=86400`
## Notes / Tradeoffs
- `/api/instance` is a temporary compromise. It exists because
`INSTANCE_ID` is currently random per boot, which is not safe to embed
into cacheable shared HTML.
- The current matchmaking flow still asks the client to provide
`instance_id` during `matchmaking/join`. That is functional, but it is
the wrong ownership boundary: instance selection should be handled by
the matchmaking service, not by the browser.
- The cleaner end-state would be:
- make `matchmaking/join` stop requiring `instance_id` from the client,
and let the matchmaking service select a healthy instance from worker
check-ins
- This branch makes the origin behavior edge-cache-friendly, but
Cloudflare still needs matching cache rules if HTML itself should be
cached at the edge.
## Validation
Verified during development with:
- `npx tsc --noEmit`
- `node node_modules\\vite\\bin\\vite.js build`
- `node node_modules\\vitest\\vitest.mjs run
tests/server/RenderHtml.test.ts tests/server/NoStoreHeaders.test.ts
tests/server/StaticAssetCache.test.ts
tests/core/configuration/ConfigLoader.test.ts`
Additional targeted tests added:
- `tests/AssetUrls.test.ts`
- `tests/core/game/FetchGameMapLoader.test.ts`
- `tests/core/configuration/ConfigLoader.test.ts`
- `tests/server/NoStoreHeaders.test.ts`
- `tests/server/StaticAssetCache.test.ts`
- `tests/server/RenderHtml.test.ts`
## Known Existing Warnings
The production build still reports pre-existing warnings that are not
addressed by this branch:
- inconsistent JSON import attributes for `resources/countries.json`
- inconsistent JSON import attributes for `resources/QuickChat.json`
- large chunk warnings from Vite
## Rollout Notes
- Cache rules should treat `/_assets/**` and `/assets/**` as immutable.
- Cloudflare will still classify HTML as dynamic after deploy unless
matching edge cache rules are configured for it.
## Follow-ups
- Remove `/api/instance` by changing `matchmaking/join` so the server
selects the target instance, or by making `INSTANCE_ID` deploy-stable if
the current contract must remain.
## Please complete the following:
- [ ] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] I have added relevant tests to the test directory
- [ ] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
DISCORD_USERNAME
## Description:
Extend NationCreation for a bit more variety :D
## 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:
Introduces 4 new private match maps for the OFM tournament in May.
Includes 2, 3, 4, and 8 player maps. Playtested, and include as many
default nations as the map expects players, named after compass points.
Give it a try at https://tourney-maps.openfront.dev/
<img width="1500" height="1500" alt="image"
src="https://github.com/user-attachments/assets/9138b636-5dd7-4118-82e2-50a5125a7963"
/>
The base images were created from scratch based on prototype designs,
and they were converted from vector versions I made. I haven't decided
on what the names should be for the maps themselves, so for now they're
just `Tourney Map 1`, `Tourney Map 2`, etc. Ideas welcome.
## 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
## Description:
When a player sends a transport ship toward another player's territory,
any pending alliance request from the target is now automatically
rejected.
This mirrors the behavior already in place for direct attacks,
preventing a player from exploiting a pending alliance request while
launching a naval invasion.
## 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
## 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.

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

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

**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
## Description:
Increases the spawn immunity duration from 30s to 45s for maps with 5M
starting gold.
The previous 30s was too short - 45s gives players 15s longer than it
takes to build a SAM, allowing them to establish basic defenses before
becoming vulnerable.
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
Increases the spawn immunity duration from 30s to 45s for maps with 5M
starting gold.
The previous 30s was too short - 45s gives players 15s longer than it
takes to build a SAM, allowing them to establish basic defenses before
becoming vulnerable.
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
Fixes https://github.com/openfrontio/OpenFrontIO/issues/1021
Fixes issue that has been there since the beginning. Player name and
location and conquest FX (swords) not being in the right place. It can
happen at any time during a game and can be game-breaking in that
regard.
This makes it hard to find players, especially when trying to eliminate
their last few tiles on some island. So when clicking name in
leaderboard > wrong tiles. And when seeing name > above wrong tiles. Bug
report:
https://discord.com/channels/1284581928254701718/1444669324571967680
Also, when removing those last tiles, the wait time between updates of
player location can make it frustrating to find and eliminate them fast.
You need 2-3 clicks on their name in leaderboard, before finally being
moved to their current location.
**Cause:**
largestClusterBoundingBox not being changed when last attack happened in
same tick removeClusters last ran.
**Fix:**
Also call removeClusters, and therefore update largestClusterBoundingBox
, when LastTileChange was AT lastCalc tick.
**Also:**
Run removeClusters if player owns less than 100 tiles, don't wait for
ticksPerClusterCalc in that case. This way, sniping off the last couple
of island tiles of the player is easier. So it doesn't take 2-3 clicks
bbut just 1 click on the player name in the Leaderboard before the
camera moves to the next little island they are on. Also their last
clusters are annexed faster, only helping with the faster cleanup.
I think this is an optional to the fix in this PR, but still an
important QoL fix for sniping those last tiles quickly.
**BEFORE:**
https://github.com/user-attachments/assets/0960a4d3-7f8b-4368-9531-8244356bff17
**AFTER:** (also notice how it now just takes 1 click in the leaderboard
to immediately go to their next location, not 2-3 clicks)
https://youtu.be/qXJPekjsrP4
## 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
## Description:
Fixes https://github.com/openfrontio/OpenFrontIO/issues/1021
Fixes issue that has been there since the beginning. Player name and
location and conquest FX (swords) not being in the right place. It can
happen at any time during a game and can be game-breaking in that
regard.
This makes it hard to find players, especially when trying to eliminate
their last few tiles on some island. So when clicking name in
leaderboard > wrong tiles. And when seeing name > above wrong tiles. Bug
report:
https://discord.com/channels/1284581928254701718/1444669324571967680
Also, when removing those last tiles, the wait time between updates of
player location can make it frustrating to find and eliminate them fast.
You need 2-3 clicks on their name in leaderboard, before finally being
moved to their current location.
**Cause:**
largestClusterBoundingBox not being changed when last attack happened in
same tick removeClusters last ran.
**Fix:**
Also call removeClusters, and therefore update largestClusterBoundingBox
, when LastTileChange was AT lastCalc tick.
**Also:**
Run removeClusters if player owns less than 100 tiles, don't wait for
ticksPerClusterCalc in that case. This way, sniping off the last couple
of island tiles of the player is easier. So it doesn't take 2-3 clicks
bbut just 1 click on the player name in the Leaderboard before the
camera moves to the next little island they are on. Also their last
clusters are annexed faster, only helping with the faster cleanup.
I think this is an optional to the fix in this PR, but still an
important QoL fix for sniping those last tiles quickly.
**BEFORE:**
https://github.com/user-attachments/assets/0960a4d3-7f8b-4368-9531-8244356bff17
**AFTER:** (also notice how it now just takes 1 click in the leaderboard
to immediately go to their next location, not 2-3 clicks)
https://youtu.be/qXJPekjsrP4
## 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
## Description:
An inverse annexation could happen where the small player (even with
0,01% tiles owned) could fully annex the large player.
**TL;DR:** basically wrong use of calculateBoundingBox in
surroundedBySamePlayer, feeding it all bordertiles, making enemyBox far
bigger than it actually was in some cases. Which resulted in enemyBox of
small player with two small clusters at some distance from each other,
being seen as inscribing the largest cluster of the bigger player. While
that largest cluster is actually the border tiles of the bigger player
surrounding the main cluster of the small player. Instead of an
annexation of small by bigger, small would incorrectly annex bigger
completely.
**Situation:** bigger player fully surrounds main cluster of smaller
player. Those border tiles are also the largest cluster of the bigger
player, for which surroundedBySamePlayer is called.
SurroundedBySamePlayer finds the small player as the only bordering
enemy of this cluster. Then it needs to check which of the two players
is surrounded by the other one. EnemyBox uses calculateBoundingBox with
all border tiles of the small player as argument. The small player also
has at least one seperate cluster elsewhere, could be on another island,
which count as border tiles too. The enemyBox from the main cluster of
the small player to the seperate cluster elsewhere, can be huge. Now
inscribed() is called and it determines that largest cluster box of the
bigger player (which was in fact calculated correctly, also making use
of calculateBoundingBox) is surrounded by the bigger enemyBox. And so
the small surrounded player fully annexes the bigger player.
**Fix:** instead of a global enemyBox, we only need the localEnemyBox
that touches the largest cluster of the bigger player. With that,
inscribed() can correctly conclude that largest cluster box surrounds
the localEnemyBox. As a matter of fact isSurrounded() already used the
same method to calculate its enemyBox as introduced by @scamiv for v30:
https://github.com/openfrontio/OpenFrontIO/pull/3127/changes#diff-fb1101a2b50dd7c353d082ff7a3351cff5469b8249b3ebca91c10573a3dfaaf1
- Change in PlayerExecution
- Added test NoInverseAnnexation.test.ts, which fails before and passes
after the fix
The bug was introduced in this commit 10 months ago:
https://github.com/openfrontio/OpenFrontIO/commit/c4381a9ad3828b06764ab1a21fc1514e37aacfd7
It has probably led to some weird annexations happening since then. The
bug could seemingly happen on any map. But was noted recently a few
times on square islands (Sierpinski) or maps (The Box/The Alps), where
the circumstances probably highten the chances of the bug occuring.
**Bug reports:**
https://discord.com/channels/1359946986937258015/1481916231689703477/1481916231689703477https://discord.com/channels/1359946986937258015/1481916231689703477/1481963273367851030https://discord.com/channels/1284581928254701718/1479993924432171008/1479995658302652496https://discord.com/channels/1284581928254701718/1479993924432171008/1481865495492956182https://discord.com/channels/1284581928254701718/1483047153571201034
**BEFORE:**
https://github.com/user-attachments/assets/4440182b-f696-45cf-bb01-b10159df8763
**AFTER**, on the same replay but with the bugfix:
https://github.com/user-attachments/assets/5f461ab2-eb62-4cc3-ae07-e2224adbbc6a
## 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
## Description:
An inverse annexation could happen where the small player (even with
0,01% tiles owned) could fully annex the large player.
**TL;DR:** basically wrong use of calculateBoundingBox in
surroundedBySamePlayer, feeding it all bordertiles, making enemyBox far
bigger than it actually was in some cases. Which resulted in enemyBox of
small player with two small clusters at some distance from each other,
being seen as inscribing the largest cluster of the bigger player. While
that largest cluster is actually the border tiles of the bigger player
surrounding the main cluster of the small player. Instead of an
annexation of small by bigger, small would incorrectly annex bigger
completely.
**Situation:** bigger player fully surrounds main cluster of smaller
player. Those border tiles are also the largest cluster of the bigger
player, for which surroundedBySamePlayer is called.
SurroundedBySamePlayer finds the small player as the only bordering
enemy of this cluster. Then it needs to check which of the two players
is surrounded by the other one. EnemyBox uses calculateBoundingBox with
all border tiles of the small player as argument. The small player also
has at least one seperate cluster elsewhere, could be on another island,
which count as border tiles too. The enemyBox from the main cluster of
the small player to the seperate cluster elsewhere, can be huge. Now
inscribed() is called and it determines that largest cluster box of the
bigger player (which was in fact calculated correctly, also making use
of calculateBoundingBox) is surrounded by the bigger enemyBox. And so
the small surrounded player fully annexes the bigger player.
**Fix:** instead of a global enemyBox, we only need the localEnemyBox
that touches the largest cluster of the bigger player. With that,
inscribed() can correctly conclude that largest cluster box surrounds
the localEnemyBox. As a matter of fact isSurrounded() already used the
same method to calculate its enemyBox as introduced by @scamiv for v30:
https://github.com/openfrontio/OpenFrontIO/pull/3127/changes#diff-fb1101a2b50dd7c353d082ff7a3351cff5469b8249b3ebca91c10573a3dfaaf1
- Change in PlayerExecution
- Added test NoInverseAnnexation.test.ts, which fails before and passes
after the fix
The bug was introduced in this commit 10 months ago:
https://github.com/openfrontio/OpenFrontIO/commit/c4381a9ad3828b06764ab1a21fc1514e37aacfd7
It has probably led to some weird annexations happening since then. The
bug could seemingly happen on any map. But was noted recently a few
times on square islands (Sierpinski) or maps (The Box/The Alps), where
the circumstances probably highten the chances of the bug occuring.
**Bug reports:**
https://discord.com/channels/1359946986937258015/1481916231689703477/1481916231689703477https://discord.com/channels/1359946986937258015/1481916231689703477/1481963273367851030https://discord.com/channels/1284581928254701718/1479993924432171008/1479995658302652496https://discord.com/channels/1284581928254701718/1479993924432171008/1481865495492956182https://discord.com/channels/1284581928254701718/1483047153571201034
**BEFORE:**
https://github.com/user-attachments/assets/4440182b-f696-45cf-bb01-b10159df8763
**AFTER**, on the same replay but with the bugfix:
https://github.com/user-attachments/assets/5f461ab2-eb62-4cc3-ae07-e2224adbbc6a
## 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
## Description:
Reduces `numSpawnPhaseTurns` from 300 to 100 (matching singleplayer
duration) when random spawn is active. Since players can't choose their
spawn location in random spawn mode, there's no need for the full
300-tick spawn phase.
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
Reduces `numSpawnPhaseTurns` from 300 to 100 (matching singleplayer
duration) when random spawn is active. Since players can't choose their
spawn location in random spawn mode, there's no need for the full
300-tick spawn phase.
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
For the next v30 fix version
<img width="868" height="364" alt="imaege"
src="https://github.com/user-attachments/assets/520a999c-67e7-4c57-8651-895ad9eeb73a"
/>
HvN balancing for the revamped difficulty steps of v30 sadly doesn't
really work out...
In medium difficulty games humans nearly always win (boring)
In hard difficulty games humans usually lose
It was intended differently...
So lets get rid of medium difficulty HvN, always use hard difficulty and
disable the donation-capability for public game nations.
That will tune the human winrate towards a middle ground at about 65% I
think. Which should be nice.
Easier than in v29 (was frustrating sometimes) but not as easy as it's
now.
We can only test this in prod lol
## 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:
For the next v30 fix version
<img width="868" height="364" alt="imaege"
src="https://github.com/user-attachments/assets/520a999c-67e7-4c57-8651-895ad9eeb73a"
/>
HvN balancing for the revamped difficulty steps of v30 sadly doesn't
really work out...
In medium difficulty games humans nearly always win (boring)
In hard difficulty games humans usually lose
It was intended differently...
So lets get rid of medium difficulty HvN, always use hard difficulty and
disable the donation-capability for public game nations.
That will tune the human winrate towards a middle ground at about 65% I
think. Which should be nice.
Easier than in v29 (was frustrating sometimes) but not as easy as it's
now.
We can only test this in prod lol
## 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:
In v30 we have the following change to prevent teammates from destroying
your structures:
**Block nuking teammate structures** - Nukes blocked if they'd hit a
teammate's structure (that was possible by nuking oceans / rivers) (by
@FloPinguin)
Original idea was from Wonder.
I think it makes sense, but it has a side effect: The aftergame, which
many players love, will be dead because of this change.
<img width="835" height="103" alt="image"
src="https://github.com/user-attachments/assets/521b7915-be28-4d83-8d45-65835e7385ab"
/>
<img width="1101" height="105" alt="image"
src="https://github.com/user-attachments/assets/db74a9c6-da12-44a2-aa06-f042b8e58b8a"
/>
I think a lot of complaints will follow after v30 is live.
So why not add a little bit of logic for the aftergame?
After a team wins/loses, players can nuke their teammates. No longer
need to aim for water. SAMs also intercept teammate nukes in this phase.
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
Now that cities snap to existing rails, it's possible to line up dozens
of cities in a row, producing way too much gold. This PR reduces the
gold after each stop to prevent that. Gold only starts decreasing after
the 3rd city.
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
evan
## Description
Follows up on #3290 which renamed the user-facing "Bots" to "Tribes".
This renames the internal implementation to match:
- `BotExecution` → `TribeExecution`
- `BotSpawner` → `TribeSpawner`
- `BotNames` → `TribeNames` (`BOT_NAME_*` → `TRIBE_NAME_*`)
All callers updated accordingly. `PlayerType.Bot` and `ColoredTeams.Bot`
are intentionally left unchanged as they are serialised wire-format
values.
Closes#3335
## Please complete the following:
- [x] My changes do not break existing functionality
- [x] I have tested my changes
## Please put your Discord username so you can be contacted if a bug or
regression is found:
Just reply here
---------
Co-authored-by: PGray <PGrayCS@users.noreply.github.com>
## Description:
Rex had this idea: "It would be funny to have an option in private
lobbies to disable alliances."
I added it as an option.
Now people can choose to live in constant fear of their neighbors 😆
Also added two new public game modifiers for variety (only for the
special rotation):
- Alliances disabled (low probability)
- x2 gold multiplier (low probability)
Would be nice to squeeze this into v30, last minute?
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FloPinguin
## Description:
Adds Aegean map, a map centered in the Aegean Sea and the aegean islands
between Greece and Turkey. Map has approx. 1M land tiles, size of
1600x2000.
Nations are named after Ancient Greece era city-states and tribes.
Elevation source from OpenTopography, already credited
<img width="1600" height="2000" alt="image"
src="https://github.com/user-attachments/assets/e4f52d6e-bba4-4641-9eac-31e3ddee354e"
/>
<img width="453" height="445" alt="Captura de pantalla 2026-03-08
141256"
src="https://github.com/user-attachments/assets/5d2bc8cf-1e54-461a-ae0f-9bb2dafc2db6"
/>
This has been a heavily requested map in the community, having
suggestions in the Discord, Subreddit and even in some videos (for
example i saw Ultimus Rex suggest an aegean map when asked what maps he
would like in his last stream)
The map is designed so that the greek and turkish main landmasses are
around 400,000 pixels in area, which combined account for around less or
equal to 80% of the map. This means players will have to cross the sea
to win, and to do so they will have to hop across the islands, which
means there will be heavy warship action, instead of having stalemates
like in most island maps where the gap between islands is too large and
players would rather bomb each other.
## 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 San Francisco bay map. 21 nations based on cities and towns of the
area. 1.8M land pixels , size of 2000x1700
Elevation data from Opentopography, already credited.
Map frequency of 3, as to mirror New York map.
<img width="2000" height="1700" alt="image"
src="https://github.com/user-attachments/assets/dc80a2db-6233-4b50-8f07-bd21c23c8b53"
/>
## 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:
clientId replay bugfix (was picking first clientID in the array)
https://discord.com/channels/1359946986937258015/1479543573404844042
## 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:
Adds Arctic map, a map centered about the Geographical North Pole using
an azimuthal equidistant projection. Features Cold War themed countries
and subdivisions as nations. Square map with 1.6M land tiles. Terrain
data from Opentopography and Arctic SDI real relief data
<img width="1830" height="1830" alt="image"
src="https://github.com/user-attachments/assets/0b8b1e42-f477-4ebf-a256-c07536db87d9"
/>
## 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:
A bunch of small UI improvements:
* Make the content width a bit smaller so gutter ads fit
* remove the "duos" "trios" "quads" description on the game card since
it's redundant
* update UI in game card
* minor footer layout changes
* update z-index to ensure content appears above ads
* removed hasUnusualThumbnailSize, instead just check the map ratio
* Use "object cover" for non-irregular maps to the entire game card is
filed
* remove white ouline from the version
* changed solo button to sky blue
* make timer "s" lowercase
I think we may need to change the openfront logo color a bit too to
match the color palette, but we can do that in a follow up.
<img width="1591" height="969" alt="Screenshot 2026-03-05 at 2 04 48 PM"
src="https://github.com/user-attachments/assets/7bb9ea4c-5a17-47e1-bdad-9d6437b363b3"
/>
## 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:
BotSpawner used the same PRNG seed (simpleHash(gameID)) as
createGameRunner, causing bot IDs to collide with nation IDs. When a
bot's SpawnExecution found a nation with the same ID via hasPlayer(), it
silently reused that nation instead of creating a new player - resulting
in far fewer players than configured (e.g. ~670 instead of 800 with 400
bots + 400 nations) with no console warnings.
Offsets the BotSpawner seed by +2 to avoid the shared PRNG sequence
(matching the +1 pattern already used by Executor).
## 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:
TL;DR: it's faster.
buildableUnits is called via PlayerView.actions from UnitDisplay (each
tick without TileRef), BuildMenu (each tick when open), MainRadialMenu
(each tick when open), PlayerPanel (each tick when open),
StructureIconsLayer (when placing a building from build bar),
NukeTrajectoryPreviewLayer (when placing nuke, on tick when tile
changes), ClientGameRunner (on click to attack/auto-boat or hotkey B or
G).
After https://github.com/openfrontio/OpenFrontIO/pull/3213 got merged,
the change with largest impact in
https://github.com/openfrontio/OpenFrontIO/pull/3193 was done in such a
different way that a new PR was needed
The idea in 3193 was to not always ask for Transport Ship from
buildableUnits. In such a way that very little extra data was send to
the worker. This had the biggest impact on performance (the idea was
months older btw, see
https://github.com/openfrontio/OpenFrontIO/pull/2295). Now, we do it the
other way around, by telling buildableUnits all unit types we want. Or
we want them all (undefined). The downside is more data is send in the
worker message. The upside is we have more options and can add more in
this PR.
This PR implements some of the leftovers in 3193 on top of 3213 and adds
further improvements.
(Some unrelated refactor/perf changes where moved out of this PR and
into already merged
https://github.com/openfrontio/OpenFrontIO/pull/3233,
https://github.com/openfrontio/OpenFrontIO/pull/3234,
https://github.com/openfrontio/OpenFrontIO/pull/3235,
https://github.com/openfrontio/OpenFrontIO/pull/3236,
https://github.com/openfrontio/OpenFrontIO/pull/3237,
https://github.com/openfrontio/OpenFrontIO/pull/3238,
https://github.com/openfrontio/OpenFrontIO/pull/3239)
- **GameRunner**, **WorkerMessages**: _playerActions_ and
_PlayerActionsMessage ._ Option to ask for no buildable units (null). It
now has 3 modes: get all actions and all buildings (units undefined),
get all actions and no buildings (units null), or get all actions and
specific building (units contains Unit Types).
- **GameRunner**: _playerActions_. fixes wrong assumption in PR 3213:
that only if units was undefined, we have to know canAttack.
ClientGameRunner wants to know both, in case of a click on non-bordering
land, to decide if it should auto-boat using a Transport Ship. So units
is not undefined (we only ask for Transport Ship now which has a
positive effect on performance for each click/tap) but we need canAttack
still.
Solved by removing the unit === undefined check before _canAttack_ in
_playerActions_.
- **GameRunner**, **GameView**, **WorkerClient**, **WorkerMessages**,
**Worker.worker**: added _playerBuildables_ / _buildables_ next to
existing _playerActions_ / _actions_. With above solved, there was still
no option to only get buildable units when the actions are not needed.
While **StructureIconsLayer**, **NukeTrajectoryPreviewLayer**,
**BuildMenu** and **UnitDisplay** need only that. To not make
playerActions more convoluted with more params or so, i've added a new
function _playerBuildables_ in **GameView** to only get buildable units
(**GameRunner** _playerBuildables_). _playerBuildables_ has 2 modes: get
all buildings (units undefined) or get specific buildings (units
contains Unit Types). Also update some comments that mentioned .actions
in **NukeTrajectoryPreviewLayer**.
- **ClientGameRunner**, **PlayerPanel**, **BuildMenu**, **UnitDisplay**,
**StructureIconsLayer** and **NukeTrajectoryPreviewLayer**: Since PR
3213, **StructureIconsLayer** and **NukeTrajectoryPreviewLayer** ask for
specific types of units from **GameView** _actions_ (**GameRunner**
playerActions). Now have the other files do the same. For example
**BuildMenu** asks for the new _BuildMenuTypes_ when it calls
._buildables_ and **ClientGameRunner** asks for UnitType.TransportShip
when sending a boat
- **ClientGameRunner**: canBoatAttack now accepts BuildableUnit[]
instead of PlayerActions so we can send it either actions.buildableUnits
or just buildables. Have functions call myPlayer.buildables(tileRef,
[UnitType.TransportShip]) when we only need a buildable unit and no
actions. Or myPlayer.actions(tileRef, null) when we need actions but no
buildable units. Or myPlayer.actions(tileRef, [UnitType.TransportShip])
when we need both actions, like canAttack, and a buildable unit. Then if
needed send either actions.buildableUnits or buildables to to
_canAutoBoat_ / _canBoatAttack_.
- **MainRadialMenu**: needs all player buildable unit types including
Transport Ship, so the _actions_ call argument for unit types can stay
undefined (unchanged) there.
- **MainRadialMenu**: now that **BuildMenu** uses _playerBuildables_
instead of _playerActions_, we must put data in
_this.buildMenu.playerBuildables_. And since we're not putting the
(unneeded) full _actions_ in there anymore, we can now put only the
needed and expected _actions._buildableUnits_ in it.
- **Game**, **PlayerImpl**, **StructureIconsLayer**: Typesafety and some
added perf: new type _PlayerBuildableUnitType_ (see also the below point
for how it is formed). So callers of _buildableUnits_ can never ask for
the wrong type like e.g. UnitType.Train because it doesn't return data
for that type. This type is now used in **PlayerImpl**, **BuildMenu**,
**RadialMenuElements**, **StructureDrawingUtils** and **UnitDisplay**
for that reason. And **InputHandler**, **StructureIconsLayer** and
**UIState** (little more on that in point below).
- **InputHandler**, **StructureIconsLayer**, **UIState**: In order to
make type safety work for GhostUnit.buildableUnit.type too (line ~217 of
StructureIconsLayer), changed type of interface _BuildableUnit_ to
_PlayerBuildableType_. Which is only more accurate. Same for and
this.structures and uiState.ghostStructure and with the latter,
_renderUnitItem_ in **UnitDisplay** and _setGhostStructure_ in
**InputHandler**. All Structures are of PlayerBuildableType (there are
even some in PlayerBuildables that aren't Structures, but it is much
more confined than UnitType).
- **Game**: Typesafety and some added perf: added _BuildMenus_ and
_BuildableAttacks_ in the same fashion that the existing StructureTypes
was already used (simplified it a bit too, with it renamed
_StructureTypes_ to _Structures_ and removed _isStructureType_). They
can be used with .types or .has(). _BuildableAttacks_.has() is used in
**RadialMenuElements**. _BuildableAttacks_ and existing _Structures_ now
make up _BuildMenus_. Which is used in **BuildMenu**,
**StructureIconsLayer** and **UnitDisplay**. Then _BuildMenus_ together
with UnitType.TransportShip make up the _PlayerBuildables_. Which is
used in **PlayerImpl** _buildableUnits_ (see point below). And with
_PlayerBuildableUnits_ we get the new _PlayerBuildableUnitType_ (see
above point on Game / PlayerImpl).
- **RadialMenuElements**: replace non-central ATTACK_UNIT_TYPES in
**RadialMenuElements** with centralized _BuildableAttackTypes_ too. Use
_PlayerBuildableUnitType_ for more type safety (can't by mistake add
UnitType.Train to its build menu). Make use of _BuildableAttackTypes_
instead of adding items hardcoded line by line in _getAllEnabledUnits_,
just like we already did since PR 3239 with _StructureTypes_. And use
_BuildableAttacks.types_ in the same fashion that existing
_isStructureTypes_ (now Structures.types) was already used elsewhere.
- **PlayerImpl**: _buildableUnits_
-- would do Object.values(UnitTypes) on every call. Now for better perf
directly loop over player buildable units by using _PlayerBuildables_
(see above point). In this way we also exclude MIRVWarhead, TradeShip,
Train, SamMissile and Shell so there are less unit types to loop through
by default. Since a player doesn't build those by themselves, they are
only build by Executions which use _canBuild_ directly and not
_buildableUnits_.
-- for more performance, do for loop instead of using .map and .filter,
no intermediate array needed nor callback overhead. We just loop over
the given units (which if undefined will contain _PlayerBuildables_).
Also pre-allocate the results array to get the most out of it, even if
V8 might already be very good at this.
-- cache config, railNetwork and inSpawnPhase so they can be re-used
inside the for loop.
-- cache cost inside the loop
-- it would check twice for tile!==null to decide to call
findUnitToUpgrade and canBuild. Now once.
-- eliminated double/triple checks for the same thing. It called
_findUnitToUpgrade_ (and with that _canUpgradeUnit_) and then _canBuild_
which both check if player has enough gold for the cost of the unit
type. And they both check if the unit type is disabled. Now we call
private functions _canBuildUnitType_, _canUpgradeUnitType_ to first do
checks on unit type level for early returns, and
_findExistingUnitToUpgrade_ to find existing unit without doing anything
extra. in a specific order to check everything only once. The public
functions _findUnitToUpgrade_ and _canBuild_ have an unchanged
functionality and we don't call them from _buildableUnits_ anymore.
-- would get _overlappingRailRoads_ and _computeGhostRailPaths_ when
canBuild was true. But this data is only meant for
**StructureIconsLayer** and it logically only uses it when placing a new
unit, not when upgrading one. Which is also commented on line 351 of
**StructureIconsLayer**. So, we now only get overlapping railroads and
ghost rails if we're not hovering to upgrade an existing unit.
- **PlayerImpl**: _findUnitToUpgrade_: unchanged functionality, but have
it call new private function _findExistingUnitToUpgrade_ to find
existing unit.
- **PlayerImpl**: _canBuild_: unchanged functionality, but have it call
new private function _canBuildUnitType_ to do the checks it first did
itself. And then new private function _canSpawnUnitType_ for the rest of
the checks. This way we can call _canBuildUnitType_ and
_canSpawnUnitType_ from _buildableUnits_ in a specific order to prevent
double/triple checks.
- **PlayerImpl**: _canBuildUnitType_: new private function to be shared
by _buildableUnits_, _canBuild_ and _canUpgradeUnit_ to be able do unit
type level checks in a specific order to prevent double/triple checks.
Via parameter knownCost, _buildableUnits_ can send it the cost it
already fetched so that it doesn't have to be fetched again. For caller
_canUpgradeUnit_, the isAlive() check (which was previously only done in
canBuild) is new but harmless, maybe even better to have also check
isAlive() on upgrade now that Nations are also upgrading which might
prevent some edge case bugs.
- **PlayerImpl**: _canUpgradeUnitType_: new private function to be
shared by _buildableUnits_ and _canUpgradeUnit_ to be able do unit type
level checks in a specific order to prevent double/triple checks.
- **PlayerImpl**: _canSpawnUnitType_: new private function to be shared
by _buildableUnits_ and _canBuildUnit_ to be able do unit type level
checks in a specific order to prevent double/triple checks.
- **PlayerImpl**: _findExistingUnitToUpgrade_: new private function to
be shared by _buildableUnits_ and _findUnitToUpgrade_ to be able do unit
level checks in a specific order to prevent double/triple checks.
- **PlayerImpl**: _isUnitValidToUpgrade_: new private function to be
shared by _buildableUnits_ and _canUpgradeUnit_ to be able do unit level
checks in a specific order to prevent double/triple checks.
- **PlayerImpl.test.ts**: because of the isAlive() check in which is new
for _canUpgradeUnit_ (see above at _canBuildUnitType_), the tests needed
to have the players be alive at the start, in order to pass.
- **BuildMenu**: use .find instead of .filter in canBuildOrUpgrade, a
function we already needed to change. This is faster and prevents an
allocation.
**PERFORMANCE**
As for calling ._buildables_ instead of unnecessarily getting
._actions_, there is an obvious win because there's less to send
calculate and recieve.
Also asking for only the needed buildings helps a lot (especially if
TradeShip isn't needed, see the difference in benchmark in original
#3193).
But the real-world impact is hard to measure. gave it a try in #3193 and
those results should be even better now.
Now testing only _buildableUnits_ performance in a synthetic benchmark,
we get these results. This is after other performance improvments so the
base is already better than it was in original #3193:
**BEFORE** (only buildableUnits itself)
<img width="602" height="96" alt="image"
src="https://github.com/user-attachments/assets/7770c0fa-a35e-42fc-90de-1de83242ec23"
/>
**AFTER** (only buildableUnits itself)
<img width="603" height="91" alt="image"
src="https://github.com/user-attachments/assets/a1578382-7010-4160-937c-7117bad18beb"
/>
## 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>