Commit Graph

1829 Commits

Author SHA1 Message Date
evanpelle 76878a91d6 make SOLO button highlight on hover 2026-03-09 09:53:55 -07:00
evanpelle ebd39e8ced cap attacks panel height and make it scrollable when overflow 2026-03-09 09:44:25 -07:00
Evan 5396931909 add footer ad to homepage (#3385)
Ad a footer ad to the bottom of the homescreen


<img width="1568" height="984" alt="Screenshot 2026-03-08 at 2 28 05 PM"
src="https://github.com/user-attachments/assets/a8009e37-778e-47f0-add8-42885d4f0c11"
/>

<img width="716" height="834" alt="Screenshot 2026-03-08 at 2 28 56 PM"
src="https://github.com/user-attachments/assets/5e910d6a-5019-4e06-ad9a-8980470371ca"
/>

<img width="862" height="834" alt="Screenshot 2026-03-08 at 2 29 31 PM"
src="https://github.com/user-attachments/assets/85e87052-ff7a-4266-8a2d-432831e3c7d6"
/>




## 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-09 09:31:02 -07:00
evanpelle eeb75342f6 improve game starting modal styling 2026-03-08 22:04:22 -07:00
Evan c63b304a97 various homepage improvements (#3387)
## Description:

Various changes, applied more styling from the homewrecker branch

* dimmed background
* Content width: expands to 24cm on 2xl screens
* game card ocean color: French blue → sky-950
* Action buttons (Create/Ranked/Join): French blue → slate-700
* Modifier badges: teal → sky blue, to keep in color scheme
* CTA buttons (Start Game, Join Lobby): blue-600 → sky-600 across all
modals and <o-button>
* Nav font: font-bold tracking-widest → font-medium tracking-wider
* Username/flag inputs: font weight lightened to font-medium
tracking-wider
* Language flag: blue color filter applied


BEFORE:


<img width="1446" height="978" alt="Screenshot 2026-03-08 at 6 48 57 PM"
src="https://github.com/user-attachments/assets/ff748e1c-6cb5-4a66-ac27-9538e935b325"
/>

AFTER:

<img width="1629" height="988" alt="Screenshot 2026-03-08 at 6 46 53 PM"
src="https://github.com/user-attachments/assets/364bb57a-65ff-40cf-931b-067ed36e3c5b"
/>


## 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-08 19:00:24 -07:00
Bartosz Woźniak 936928fed9 Enhance InputHandler to allow using NumPad (#3317)
## Description:

Adds **Enter** and **Numpad Enter** as confirmation for placing a ghost
structure after selecting a building with hotkeys (1–0 or numpad).
Players can cancel with Esc but previously had to click to confirm; they
can now confirm with Enter or Numpad Enter at the current cursor
position. This supports keyboard-only or mouse + numpad workflows (e.g.
one hand on numpad for select + confirm, one on mouse for aiming).

## 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-07 15:27:25 -08:00
FloPinguin fe89713f46 Fix UI (again) 🖌️ (#3379)
## Description:

 **Fix UI spacing and border radius across multiple screen sizes**

- Fix events panel missing right margin on wide screens
- Fix incorrect border radius on events panel and control panel at
various breakpoints
- Remove border radius from attack/boat elements on small screens
- Show running attacks above the events panel on mobile
- Add left/right margin to the homepage on tablet-sized screens
- Adjust lobby card spacing on mobile

Previous

<img width="410" height="124" alt="Screenshot 2026-03-07 203244"
src="https://github.com/user-attachments/assets/d3feb9fe-97a3-44d0-9aba-db04062f9911"
/>


After

<img width="417" height="117" alt="Screenshot 2026-03-07 203255"
src="https://github.com/user-attachments/assets/31b88145-8e92-40db-b9cc-f2a00754f900"
/>


Previous

<img width="828" height="123" alt="Screenshot 2026-03-07 203320"
src="https://github.com/user-attachments/assets/4e162cf5-7d82-4e87-9dd9-9ab1d3782f23"
/>


After

<img width="820" height="126" alt="Screenshot 2026-03-07 203337"
src="https://github.com/user-attachments/assets/a25121aa-603c-41c7-b335-406a38a62cf9"
/>


Previous

<img width="961" height="102" alt="Screenshot 2026-03-07 203353"
src="https://github.com/user-attachments/assets/22ba9770-88a3-4f49-aeb6-6d875006946b"
/>


After

<img width="954" height="78" alt="Screenshot 2026-03-07 203403"
src="https://github.com/user-attachments/assets/0d4e3b19-de1c-4211-b1e3-bd935025de33"
/>


Previous

<img width="557" height="154" alt="Screenshot 2026-03-07 203450"
src="https://github.com/user-attachments/assets/2cc8a747-3e68-4449-9746-62fcbca76510"
/>


After

<img width="602" height="146" alt="Screenshot 2026-03-07 203421"
src="https://github.com/user-attachments/assets/bae399a3-8969-4b7a-a77c-c73c4f775ca0"
/>


Previous

<img width="727" height="889" alt="Screenshot 2026-03-07 204707"
src="https://github.com/user-attachments/assets/bc53febf-9beb-4195-a994-858333f30f24"
/>


After

<img width="725" height="799" alt="Screenshot 2026-03-07 204714"
src="https://github.com/user-attachments/assets/9d600212-73ae-4566-b1c5-df83e8edb8e9"
/>


Previous

<img width="658" height="890" alt="Screenshot 2026-03-07 204633"
src="https://github.com/user-attachments/assets/6c935fcc-3e46-4706-8c9a-9840cc469b60"
/>


After

<img width="656" height="798" alt="Screenshot 2026-03-07 204639"
src="https://github.com/user-attachments/assets/8e490f29-cf50-4c1f-a97e-f550fd4f9a13"
/>

## 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-07 13:01:18 -08:00
FloPinguin 526cb723d6 Fix 2 HvN UI bugs 🔧 (#3378)
## Description:

I noticed two HvN bugs.

1. Private lobbies don't set `maxPlayers` in `GameConfig`, causing
`getGameModeLabel()` to render "0 Humans vs 0 Nations". Fall back to the
simple "Humans vs Nations" label when `maxPlayers` is unavailable.

<img width="239" height="84" alt="Screenshot 2026-03-07 034150"
src="https://github.com/user-attachments/assets/b2f01b96-674f-47dc-ae03-06bec71e3134"
/>

2. In public HumansVsNations games, the server matches the nation count
to the human player count at game start. The lobby team size preview
wasn't reflecting this - it displayed the raw config value instead.
Added `isPublicGame` prop to `LobbyPlayerView` and an
`effectiveNationCount` getter that overrides the displayed nation count
to match `clients.length` only for public HvN games. Private lobby hosts
retain full slider control. (This bug got introduced with my
"Configurable nation count" 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-07 12:57:16 -08:00
FloPinguin 3fca25f421 Skip multi-tab detection during replays 🛠️ (#3366)
## Description:

Multi-tab detection was incorrectly penalizing users watching replays.
Added `isReplay()` check to `MultiTabModal.tick()` so the detector is
never initialized when viewing a replay.

## 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-06 20:02:03 -08:00
DevelopingTom 902a0b42ac Remove useless sprite setting (#3363)
## Description:

Redundant animated sprite setting: the sprite frame width can be
computed directly.

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

IngloriousTom
2026-03-06 19:58:14 -08:00
Evan 815f1de67b Update control panel UI (#3357)
Relates to #2260

## Description:

Inspired by https://github.com/openfrontio/OpenFrontIO/pull/3359

This PR centers the control panel and combines it with the units
display. The reasoning is that the control panel contains the most
critical info so it should be in the center of the screen. Combining it
with the units display reduces the number of UI components on screen.

Also made the attack ratio bar persistent on mobile

<img width="618" height="216" alt="Screenshot 2026-03-06 at 2 06 34 PM"
src="https://github.com/user-attachments/assets/34b041c1-d78b-46b5-a7ab-f2a44145a7e2"
/>


<img width="941" height="343" alt="Screenshot 2026-03-06 at 2 06 55 PM"
src="https://github.com/user-attachments/assets/1e3b026c-8eb2-407c-be38-0e71e1ae426c"
/>

<img width="562" height="228" alt="Screenshot 2026-03-06 at 4 11 20 PM"
src="https://github.com/user-attachments/assets/56eac49f-c8a4-4ac1-a60a-f1bcb2fad2d0"
/>

<img width="939" height="357" alt="Screenshot 2026-03-06 at 4 11 32 PM"
src="https://github.com/user-attachments/assets/eb5591d5-3cc2-4182-944b-3a4b0b76852a"
/>


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

Co-authored-by: hkio120 <111693579+hkio120@users.noreply.github.com>
2026-03-06 18:32:01 -08:00
Ryan 0eb23c0c8c clientId replay bugfix (was picking first clientID in the array) (#3369)
## 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
2026-03-06 13:58:10 -08:00
FloPinguin c594487f5e Starting gold input in millions with decimal support (#3349)
## Description:

**Starting gold input: use millions**

Changes the starting gold input in singleplayer and host lobby modals to
accept values in millions (e.g. enter `5` for 5M gold). Supports
decimals like `6.6` for 6.6M. The value is multiplied by 1,000,000
before being sent to the game config.

- Label updated to "Starting Gold (Millions)"
- Input uses float parsing with min 0.1, matching gold multiplier
behavior
- JoinLobbyModal shows clean values without unnecessary decimals (e.g.
"5M" not "5.00M")

Previous

<img width="215" height="139" alt="image"
src="https://github.com/user-attachments/assets/00ce5b6d-f74d-4aee-92f5-c9be1a0a6d3d"
/>
<img width="292" height="74" alt="image"
src="https://github.com/user-attachments/assets/4de936a3-22bd-4ffc-8dbe-0d5066f28186"
/>

Now

<img width="216" height="151" alt="image"
src="https://github.com/user-attachments/assets/489de13e-65b5-4b02-a654-5f6f74b165d1"
/>
<img width="292" height="72" alt="image"
src="https://github.com/user-attachments/assets/51723d5a-55ab-4b7b-bbce-011a586eeb44"
/>

## 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-05 20:48:03 -08:00
FloPinguin 0dc520b1c8 Add confirmation dialog before closing host lobby modal 🔧 (#3364)
## Description:

Show a confirm prompt when the user tries to close the host lobby via
click-outside or Escape key, preventing accidental lobby exits. Happened
to many people and also DougDoug...

Does not show the prompt when the user clicks the arrow button because
it's probably intentional.

## 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-05 20:39:22 -08:00
Evan 0733c680b9 homepage UI improvements (#3352)
## 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
2026-03-05 15:17:28 -08:00
Ryan b3c01d4c85 improve streamer mode (#3353)
## Description:

improves streamer mode (doesn't show the gameID in the url, it just says
"streamer-mode"


## 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-04 19:27:53 -08:00
VariableVince e137fcaa6c Fix/Perf/Refactor: playerActions and buildableUnits, their callers and related types (#3220)
## 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>
2026-03-04 11:32:45 -08:00
FloPinguin 0b9d43cb46 Configurable nation count 🤖 (#3338)
## Description:

I hope we can get this into v30?
The nation count is configurable now, just like the bot count.
Replaced the "Disable Nations" toggle with a nations slider (0–400) in
SinglePlayer and Host Lobby modals.

<img width="710" height="121" alt="Screenshot 2026-03-03 021952"
src="https://github.com/user-attachments/assets/c8d0f0c3-db51-4303-95fa-dbc770460ec2"
/>


Public games are staying exactly the same, this is just for singleplayer
and private lobby fun.
Youtubers could play HvN against 400 nations, for example.
Singleplayer enjoyers no longer have to play against 1 nation in HvN,
they can freely choose.

`GameConfig.disableNations: boolean` got replaced by `nations: number
(0-400, optional)`
`undefined` = map default, 
`0` = disabled, 
number = custom count

Nations slider defaults to the map's nation count, shows "(MAP DEFAULT)"
label when unchanged
Compact map toggle reduces nations to 25% when at default, restores when
toggled off (just like we already do with bots)
The nation count for HvN no longer automatically matches the human count
in singleplayer and private games, only in public games.

**What if there aren't enough nations configured for the map?**
We just use the HvN logic (Generate random nations)

### Warning

**This infra PR also needs to get merged:
https://github.com/openfrontio/infra/pull/263
Otherwise players can set 0 nations and get achievements.**

## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

FloPinguin
2026-03-03 14:07:06 -08:00
scamiv 28bbd933a4 Revert "fix: resolve drawImage scaling penalty on non-square sprite height" (#3337)
Reverts openfrontio/OpenFrontIO#3320

doesnt do what it says

The #3320 description claimed it “resolves a performance parsing
penalty” and fixes “non-square sprite” scaling/ghosting issues.
In reality, the code change was limited to:

* **clearRect**: switched from clearing a `clearsize x clearsize` square
(`clearsize = sprite.width + 1`) around `lastX/lastY` to clearing a
**(sprite.width+2) x (sprite.height+2)** rect around **rounded**
`clearX/clearY` (with an extra 1px pad via `-1/+2`).
* **drawImage**: changed a single call’s destination height from
`sprite.width` → `sprite.height`.

### Why revert

For unit rendering, sprites are square, so the drawImage change is a
no-op in practice, and the main effect was **clearing more pixels per
frame**. Any theoretical gain from rounding coordinates is speculative,
and is outweighed by the increased clear area/overdraw.
2026-03-03 11:30:22 -08:00
VariableVince 1d1b076672 Rename/fix: change Bots to Tribes (#3290)
## Description:

Resolves #3285. As discussed on Discord.

However, in at least one instance "Tribes" feels a bit off: in Humans vs
Nations, team "Tribes" feels as human too while they are just bots.

This PR changes Bots to Tribes outwardly by 
- Changing default EN translation.
- Changing (untranslated) alt text in PlayerPanel.
- To change "Team Bot" into "Team Tribes" too in PlayerInfoOverlay and
TeamStats (team leaderboard in-game), translate team names in there from
now on too.
- This way we also fix a bug where team names were not translated yet in
there. To add to that fix, also translate team names in LobbyPlayerView
in the same way. For this we re-use the existing
getTranslatedPlayerTeamLabel function from GameLeftSideBar by moving it
to Utils.
- No translation key was present yet for Humans and Nations teams, so
added those to now be used in PlayerInfoOverlay, LobbyPlayerView and
TeamStats for completeness.
- No internal code changes so nothing breaks.

**BEFORE (showing old team name Bot and also that team names weren't
translated yet in TeamStats)**
![No translation yet in
TeamStats](https://github.com/user-attachments/assets/38f465bc-ef82-4474-806c-015bb640d233)

![No translation yet in TeamStats
2](https://github.com/user-attachments/assets/a4387f1e-0e80-491d-b57d-e52b3c616e2b)


**AFTER** (translations in Dutch only shown as proof here, did not
include nl.json in the PR)
![AFTER translated in TeamStats for Humans vs Nations as an example in
NL
json](https://github.com/user-attachments/assets/1a7dcf4e-4263-4d6b-a992-58cb08a4fa7b)
![AFTER Tribe as player type in
PlayerInfoPanel](https://github.com/user-attachments/assets/6fd09686-320e-4fee-9c0d-397e581aa676)
![AFTER translated Team name PlayerInfoPanel as an
example](https://github.com/user-attachments/assets/1b4bc684-9ef4-47a9-b91c-4ed5cda65e9e)
![AFTER Tribes in EN now that it is translated in TeamStats so fetched
from EN
json](https://github.com/user-attachments/assets/5ea6528b-7e3c-4c6e-abeb-2769fb0aedee)
![AFTER Instructions example of changed text
](https://github.com/user-attachments/assets/6c7a7ab7-1dea-4f11-bacf-3e2edcdb074b)



## 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-02 18:20:10 -08:00
Ryan 29e1ca2bda new homepage (#3332)
## Description:

updated homepage to make ffa the primary focus. closes
https://github.com/openfrontio/OpenFrontIO/issues/3288 and closes
https://github.com/openfrontio/OpenFrontIO/pull/3328
<img width="1911" height="924" alt="image"
src="https://github.com/user-attachments/assets/f81a3471-6a24-44a5-baf9-c2fdc5a3cbc3"
/>

<img width="416" height="846" alt="image"
src="https://github.com/user-attachments/assets/0456423b-4418-4719-9236-d12cb3aa1c37"
/>

## 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-02 18:01:49 -08:00
Ryan 17c0dde45f ui polish (#3333)
## Description:

- disabled dragging in many places, select skin, select flag, select
lang, and footer stuff
- removed shadow from flags in flag selector
- added bounce to the lang selector

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

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-02 18:00:34 -08:00
Skigim 60f69a6408 perf: remove O(n) StructureIconsLayer render lookups (#3305)
Begins work on #3207

## Description:

This PR is the first optimization slice for #3207: it removes O(n)
render lookups in `StructureIconsLayer` by replacing array-first render
state with a unit-id keyed map, and tightens hot-path execution to
reduce per-tick allocations.

### What changed
- Refactored render state from array-first to `rendersByUnitId:
Map<number, StructureRenderInfo>`.
- Replaced O(n) lookup/delete paths with O(1) `Map#get` / `Map#delete`.
- Replaced `seenUnits` object-identity tracking with `seenUnitIds:
Set<number>`.
- Removed `tick()` array/closure chain (`map(...).forEach(...)`) and
switched to index-based loop.
- Reduced ghost-path allocation pressure by reusing a layer-level `Set`
for connected ally IDs instead of allocating `filter` + `map` + `new
Set` per ghost query.
- Added dirty-flag caching for structure visibility focus
(`visibilityStateDirty`) so expensive visibility-state scans recompute
only when toggles change.

### Performance validation (before/after)
Benchmark added: `tests/perf/StructureIconsLayerLookupPerf.ts`

Command:
`npm run perf`

Observed result:
- `StructureIconsLayer BEFORE (array O(n) lookup/delete) x 0.33 ops/sec
±13.28%`
- `StructureIconsLayer AFTER (unit-id map O(1) lookup/delete) x 95.65
ops/sec ±2.46%`
- Fastest implementation: AFTER (unit-id map)

#### Profiler screenshots are too noisy to be useful for such a focused
change

### Verification
- `npx tsc --noEmit` 
- `npx eslint src/client/graphics/layers/StructureIconsLayer.ts
tests/perf/StructureIconsLayerLookupPerf.ts` 
- `npm run perf` 

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

skigim
2026-03-02 17:40:15 -08:00
Skigim 17f32a590c fix: resolve drawImage scaling penalty on non-square sprite height (#3320)
## Description:

This PR resolves a performance parsing penalty in the `UnitLayer`
rendering loop by fixing two issues with non-square sprites:
1. `drawImage` was incorrectly using `sprite.width` for both width and
height, causing aspect ratio squashing and forcing the browser to
perform a scaling operation on the image instead of hitting the canvas
fast-path. It now correctly uses `sprite.height` for the vertical
dimension.
2. `clearUnitsCells` was previously configured to only clear a square
grid (`clearsize`) based solely on width, meaning taller sprites would
leave visual artifact "ghosts" on the map. The clearing bounds have been
corrected to leverage discrete `sprite.width` and `sprite.height`.
Additionally, these values are wrapped in `Math.round()` prior to offset
calculation to prevent subpixel anti-aliasing CPU penalties during
`clearRect`.

## Please complete the following:

- [x] I have added screenshots for all UI updates (not needed)
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file (no new text)
- [x] I have added relevant tests to the test directory (existing tests
suffice, change was minuscule and non-breaking)
- [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:
skigim
2026-03-02 10:56:37 -08:00
Skigim f7598369ed refactor: consolidate platform detection across client components (#3325)
## Description:

This PR consolidates ad hoc platform/environment/viewport detection into
a single shared utility. It is scoped to this refactor only, and serves
as groundwork for the mobile-focused feature work planned for the v31
milestone.

### What changed
- Introduced a shared `Platform` utility centralising:
  - OS detection (with `userAgentData` + UA fallback)
  - Electron environment detection
- Viewport breakpoint helpers (`isMobileWidth`, `isTabletWidth`,
`isDesktopWidth`)
- Replaced duplicated inline checks across client files with the shared
API.
- Normalised Mac detection to derive from the consolidated OS logic
rather than a separate regex.

### Why
- Multiple client files each independently ran `navigator.userAgent`
regexes or copy-pasted `isElectron` logic — this unifies all of that.
- Puts a stable, tested abstraction in place before v31 mobile work
lands, so mobile feature branches have a consistent surface to build
against.

## Please complete the following:

- [x] I have added screenshots for all UI updates (N/A: refactor only,
no visible UI changes)
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file (N/A: no new user-facing strings)
- [x] I have added relevant tests to the test directory (N/A: refactor
only)
- [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:

skigim
2026-03-02 10:12:48 -08:00
scamiv 50197e7254 perf(PerformanceOverlay): reduce per-render overhead (#3295)
## Description:

Cache translated UI labels per language/translation load
Avoid per-frame layer breakdown sorting unless expanded
Use rolling sums instead of array reduce
Drop redundant requestUpdate() calls and object clones

## 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-01 20:15:31 -08:00
FloPinguin 417fa0fe09 For v30: Add new modifiers (Hard nations and 25M Starting Gold) 🙂 (#3316)
## Description:

Adds two new public game modifiers for variety and improves compact map
eligibility for team games.

### New Modifiers

**Hard Nations (`isHardNations`)**
- We need this modifier for HvN, because medium nations are easier now
(will result in a much higher human winrate)
- In a discord discussion we concluded that HvN should generally be
easier (higher winrate than 50%, so players are less frustated)
- Thats why only 20% of HvN games have the hard nations modifier (for
now)
- For PvPvE enjoyers, the modifier is also active in FFA games => (Only
2.5% chance, and 1 ticket in `SPECIAL_MODIFIER_POOL`)

**25M Starting Gold (`startingGoldHigh`)**
- Some people in the main discord wanted this modifier, and it will
result in crazy games
- Rare special-only modifier (1 ticket in pool); mutually exclusive with
5M starting gold via `MUTUALLY_EXCLUSIVE_MODIFIERS`
- Disables nations (they lack PVP immunity, so 25M gold doesn't work
well with them)
- Excluded from HumansVsNations games (since it disables nations)
- Spawn immunity set to **2 minutes 30 seconds** (vs 30s for 5M gold),
so people can spend the gold and prepare

### Other Changes

- **Improved `supportsCompactMapForTeams`**: Replaced the hard `smallest
>= 50` land-tile cutoff with a per-team-config calculation that
simulates worst-case compact player count and checks every team gets at
least 2 players.
- **HvN spawn immunity**: Always 5 seconds in both regular and special
lobbies (to get rid of a confusing PVP immunity HeadsUpMessage in 5M
starting gold games)
- **Regular public lobby random spawn modifier probabilty**: Reduced
from 10% to 5% (Because of the new modifier, so there aren't too many
modifiers in non-special-lobbies, should only occur sometimes there)
- Rebalanced `SPECIAL_MODIFIER_POOL` 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:

FloPinguin
2026-03-01 20:12:38 -08:00
bijx 8754f5291f Feat: Alphanumeric Coordinate Grid on Alternate View (#2938)
## Description:

Adds a coordinate grid to the Alternate View (holding spacebar) using
numbers on the X-axis, and letters on the Y-axis. No more "he's
attacking you in that—well, the little peninsula thing... next to the
island! which island? uhh..." moments when playing with friends.
Optimally maps have letters A-J (just like in the Battleships board
game) but special maps like Amazon River dynamically resize to only have
2 letters so as to not have too many number columns. This feature
overall can be toggled via the settings menu.

Also saw it requested on the [official
discord](https://discord.com/channels/1359946986937258015/1457037351422263480)
a couple times, thought it was a neat idea.

### World Map
<img width="3809" height="1824" alt="image"
src="https://github.com/user-attachments/assets/dab56879-a34e-48ea-a588-2907d26feb45"
/>

### Scales correctly when zoomed in
<img width="3798" height="1874" alt="image"
src="https://github.com/user-attachments/assets/7e06a47f-d3d9-4f92-8e89-3eaf866e9b25"
/>

### Amazon River
<img width="3803" height="1595" alt="image"
src="https://github.com/user-attachments/assets/4797c576-20b2-4aa8-8b7a-107078ab6308"
/>

### Enable/Disable via settings


https://github.com/user-attachments/assets/ec9f4e07-70a1-4f9d-b137-c3c3d2a2540c

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

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
2026-02-28 20:28:47 -08:00
scamiv 9fc11b7b9a perf(worker): remove heartbeat; batch game updates (#3308)
## Description:
Removes the client-driven heartbeat loop and switches worker tick
execution to a worker-owned drain scheduler with batched game update
delivery.

## Why
The previous flow required the client to send a `heartbeat` every
animation frame just to keep the worker progressing turns. That had two
costs:

1. Simulation progress was coupled to browser frame cadence.
2. Catch-up periods produced many single `game_update` messages,
increasing message overhead and main-thread wakeups.

## What Changed

### 1) Remove heartbeat protocol
- Deleted `heartbeat` from `WorkerMessageType`.
- Removed `HeartbeatMessage` from `MainThreadMessage`.
- Removed `sendHeartbeat()` from `WorkerClient`.
- Removed the `requestAnimationFrame` keep-alive loop in
`ClientGameRunner`.

Files:
- `src/client/ClientGameRunner.ts`
- `src/core/worker/WorkerClient.ts`
- `src/core/worker/WorkerMessages.ts`
- `src/core/worker/Worker.worker.ts`

### 2) Add batched worker-to-client updates
- Added `game_update_batch` message type and `GameUpdateBatchMessage`.
- Worker now emits one batch message containing multiple tick updates.
- `WorkerClient` handles `game_update_batch` by replaying updates to the
existing callback in order.

Files:
- `src/core/worker/WorkerMessages.ts`
- `src/core/worker/WorkerClient.ts`

### 3) Move tick draining into worker
- Added a scheduler (`scheduleDrain`) and drain loop (`drain`) in
`Worker.worker.ts`.
- On each `turn` message, worker enqueues turn and schedules drain.
- Drain executes up to `MAX_TICKS_BEFORE_YIELD = 4` ticks per cycle,
then yields with `setTimeout(..., 0)`.
- Tick updates are collected into a batch and sent once with
transferables:
  - `packedTileUpdates.buffer`
  - `packedMotionPlans.buffer` (when present)
- If backlog remains, drain reschedules itself.

File:
- `src/core/worker/Worker.worker.ts`

## Behavioral Notes
- No server protocol changes.
- Ggame update callback contract remains the same (still receives one
`GameUpdateViewData` at a time in order).
- Ordering is preserved: `WorkerClient` iterates batch entries in
sequence.
- Error updates are still filtered from update delivery in the worker
batch path (same effective behavior as before for normal update flow).

## Expected Impact
- Fewer `postMessage` calls during backlog and burst turn delivery.
- Lower message overhead and fewer main-thread interrupts.
- Less dependence on UI frame timing for worker progress.
- Better catch-up stability due to explicit periodic yielding.

## Risk Areas
- Drain scheduling edge cases (re-entrancy / lost wake-ups).
- Mitigated with `drainScheduled`, `draining`, and `drainRequested`
flags.
- Larger per-message payloads due to batching.
  - Bounded by `MAX_TICKS_BEFORE_YIELD`.
- Any assumptions in downstream code about receiving only `game_update`.
  - Handled by adding `game_update_batch` support in `WorkerClient`.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

DISCORD_USERNAME
2026-02-28 21:58:32 +00:00
scamiv c911bfb2d8 Packed unit updates / MotionPlans (#3292)
## Description:

Reduce per-step `Unit` update traffic by shipping packed motion plans
and letting the client advance plan-driven units locally.

Changes:
- Add packed motion plan records (`packedMotionPlans?: Uint32Array`) to
game updates and transfer the buffer worker -> main.
- Introduce `src/core/game/MotionPlans.ts` (schema + pack/unpack) for
grid + train motion plans.
- Extend `Game` with `recordMotionPlan(...)` and
`drainPackedMotionPlans()`, and implement buffering/packing in
`GameImpl`.
- Treat units with motion plans as “plan-driven”: suppress per-tile
`Unit` updates on `move()` and advance positions client-side.
- Emit motion plans from executions:
- `TradeShipExecution`: record/update grid motion plans and `touch()`
when changing target after capture.
- `TransportShipExecution`: record initial plan and update it when
destination changes.
  - `TrainExecution`: record a train plan on init (engine + cars).
- Client: apply motion plans in `GameView` and ensure `UnitLayer`
updates sprites for motion-planned units even when no `Unit` updates
arrived.

## 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-02-27 20:54:42 -08:00
scamiv 1cafc6bc25 perf(translateText): speed up translateText (#3296)
## Description:

Cache lang-selector lookup
Avoid per-call empty params allocation
Add fast-path for non-ICU strings

## 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-02-27 18:20:04 -08:00
evanpelle f09177f8fe Merge branch 'v29' 2026-02-26 17:40:57 -06:00
Aotumuri a7b137b3b7 fix: place select controls below text (#3299)
## Description:

Updated `setting-select` layout to a vertical flow:
  - Header
  - Description
  - Selector

before
<img width="1306" height="770" alt="スクリーンショット 2026-02-26 19 10 36"
src="https://github.com/user-attachments/assets/7da2a9af-b8bd-4f7f-8cd6-f22946d07720"
/>
<img width="372" height="749" alt="スクリーンショット 2026-02-26 19 14 18"
src="https://github.com/user-attachments/assets/50148101-4c9e-4db5-b6c3-53f819ee9e6a"
/>

after
<img width="1470" height="827" alt="スクリーンショット 2026-02-26 19 10 01"
src="https://github.com/user-attachments/assets/9e36420b-a616-4056-8b11-ebb4bf25a5b2"
/>
<img width="692" height="832" alt="スクリーンショット 2026-02-26 19 10 15"
src="https://github.com/user-attachments/assets/3b3e8fbf-fd57-47c1-9c87-763df81d673a"
/>


## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:
aotumuri
2026-02-26 10:24:22 +00:00
Mattia Migliorini 7b785ea79a Fix alliance renewal prompt incorrectly dismissed for both players (#3297)
## Description:

NOTE: Applies to current main / beta version. Needs to be included in
v30.

When a player clicked "Renew Alliance", the `AllianceExtensionUpdate`
broadcast caused both players' renewal prompts to be removed, even the
one who hadn't yet acted. This happened because
`onAllianceExtensionEvent` called `removeAllianceRenewalEvents`
unconditionally on every client.

This PR fixes the behavior by calling `removeAllianceRenewalEvents` only
for the player that executed the action.

## 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-02-25 21:12:58 -06:00
Aotumuri bd3db55a22 Add configurable attack ratio keybind increment setting (#2835)
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #2822

## Description:

Adds an attack ratio keybind increment setting with a new dropdown UI,
wires keybinds to use the configured step, updates the attack ratio
adjustment logic, and makes the select reflect stored settings.

<img width="806" height="165" alt="スクリーンショット 2026-01-12 9 11 12"
src="https://github.com/user-attachments/assets/c6eaa96d-e147-4927-b3ed-964e832ecc36"
/>

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:
aotumuri

---------

Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
Co-authored-by: iamlewis <lewismmmm@gmail.com>
2026-02-25 23:31:36 +00:00
Evan 7f03072e9b revert skin trials (#3293)
## Description:

Skin trials has been a failure, very low fill rate and cause a major
drop in sales.

reverts 


https://github.com/openfrontio/OpenFrontIO/commit/97d0a05d58e926e3de4ba46d8dd14a04d60d6698

## 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-02-24 15:47:21 -06:00
FloPinguin 339ace0bd6 v30 nuke wars preparation: Disable boats & Team spawn zones (#3263)
## Description:

Preparation for nuke wars, for v30.
Next PR will be adding the nuke wars modifier for public games, but
Wonders https://github.com/openfrontio/OpenFrontIO/pull/3224 needs to be
merged first to avoid merge conflicts.

### 1. Disable boats setting

It's possible to disable `UnitType.TransportShip` now. Because they are
not needed in nuke wars and can even be annoying.

<img width="720" height="320" alt="image"
src="https://github.com/user-attachments/assets/661bc10d-b204-4b4f-b876-ee7c9b92de8c"
/>

### 2. Team spawn zones for random spawn

Maps can have `teamGameSpawnAreas` in their json file now.
Spawn areas are currently active if 
- a supported map is chosen (Baikal Nuke Wars or Four Islands)
- a supported team size is chosen (2 teams on Baikal Nuke Wars or 2/4
teams on Four Islands)
- random spawn is enabled

## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

FloPinguin
2026-02-23 16:12:24 -06:00
scamiv 4b917c4153 Performance Overlay rework/redesign (#3274)
## Description:
updates the Performance Overlay to be more usable
(draggable/resizable/scrollable), adds tick-level metrics (TPS +
per-layer tick timings), and reduces overhead when the overlay is
hidden.

### UI/UX
- Overlay layout updated to a fixed, pixel-positioned panel (default
near top-left) with a dedicated drag handle.
- Overlay is touch-draggable (pointer events) and remains usable on
small viewports via internal scrolling.
- Overlay width is resizable with a right-edge handle; width is clamped
to viewport bounds.
- Render/tick layer breakdown sections are collapsible, with headers and
“last tick” summaries.

### New metrics
- Adds TPS reporting:
  - Current TPS (ticks in the last 1s).
- Average TPS over the last ~60s, computed using elapsed time so it’s
accurate before a full 60s passes.
- Adds per-layer tick profiling (“Tick Layers”) alongside render
profiling (“Render Layers”).
- Adds “render-per-tick” metrics so render-layer costs can be understood
per simulation tick (frames + per-layer totals).

### Performance / overhead
- Avoids profiling overhead when the overlay is hidden:
- `GameRenderer` only calls `FrameProfiler.clear()/consume()` and
per-layer `start/end` when profiling is enabled.
- Tick-layer duration tracking is only collected when profiling is
enabled.

### Settings plumbing
- `UserSettings` now dispatches a `user-settings-changed` `CustomEvent`
on `set()` / `setFloat()`.
- The overlay listens for `settings.performanceOverlay` changes so
visibility stays in sync even when toggled outside the overlay.

## Implementation notes (by file)

- `src/client/graphics/layers/PerformanceOverlay.ts`
  - Adds TPS tracking using a timestamp ring + moving heads (1s / 60s).
- Adds UI state for collapsibles, drag + resize pointer tracking, and
new breakdown models:
    - Render layers: EMA avg/max + per-tick render aggregation.
    - Tick layers: EMA avg/max + last-tick durations.
- Copy-to-clipboard snapshot now includes TPS, tick layers, and
render-per-tick last-tick details.

- `src/client/graphics/GameRenderer.ts`
  - Gates render-layer profiling behind `FrameProfiler.isEnabled()`.
- Accumulates per-render-layer timings across frames and publishes them
once per tick via `updateRenderPerTickMetrics(...)`.
- Measures tick-layer durations (per layer `tick()` call) and publishes
them via `updateTickLayerMetrics(...)`.

- `src/core/game/UserSettings.ts`
- Adds `emitChange(key, value)` to dispatch `user-settings-changed` to
`globalThis` (best-effort).

- `resources/lang/en.json`
- Adds/updates `performance_overlay.*` strings for TPS and the new
render/tick layer sections.

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

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-23 14:22:56 -06:00
Nolhan e5ce278cb1 refactor: enhanced Join Private Lobby form (#3284)
## Description:
This pull request enhances the `JoinLobbyModal` component by using the
`<form>` component and the `@submit` event. It allows the user to use
the enter (return) key to submit instead of grabbing its mouse to click
on "Join Lobby".
It also introduces a new `submit` argument to the `Button` component.

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

PS: The tests from `tests/InputHandler.test.ts` are failing on both
`main` and my branch. EDIT: They no longer fail through the workflow so
I guess I didn't have the correct environment
2026-02-23 19:02:24 +00:00
FloPinguin b1c4c9723c Followup leaderboard fix... (for mobile) (#3281)
## Description:

Column widths were off for some reason, I thought they were fixed...
So here is a followup PR

Previous:

<img width="467" height="782" alt="image"
src="https://github.com/user-attachments/assets/f5a084ea-e8b9-473b-abe4-d8c9d0d5d9de"
/>

Now:

<img width="454" height="779" alt="image"
src="https://github.com/user-attachments/assets/d845ec32-e76e-4ad5-aa62-5642a4c78da4"
/>

## 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-02-22 21:52:50 -06:00
FloPinguin edc3e20a9f Improve 1vs1 ranked leaderboard (#3270)
## Description:

The two tables look much more similar now
And you can see the player names now

Before:


https://github.com/user-attachments/assets/59f94e1a-5909-4d13-8ff3-bd36775f4ae6

After:


https://github.com/user-attachments/assets/51234d14-20c2-4b14-a7cc-ceef7cf9a8fd

## 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-02-22 18:52:59 +00:00
FloPinguin 7c6c2b1fd8 Adjust bottom margin for lobby card layout on homepage (#3268)
## Description:

Before:

<img width="744" height="540" alt="image"
src="https://github.com/user-attachments/assets/79baafa3-0c80-470d-a7bc-da428a0d4402"
/>

After:

<img width="746" height="509" alt="image"
src="https://github.com/user-attachments/assets/ca3c57d4-0854-4879-9792-ee8e00ae164e"
/>

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

Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
2026-02-22 14:35:05 +00:00
FloPinguin 2fd8757e66 Notification dot for new versions (+ mobile dot improvements) (#3265)
## Description:

- **News notification dot (desktop + mobile)**: Added a red pinging dot
on the "News" nav entry that appears when a new version is released. The
current app version is saved to localStorage (`newsSeenVersion`) on
first visit. On subsequent visits, if the version has changed, the dot
appears. Clicking "News" dismisses it by updating the stored version.

- **Mobile Store**: Replaced the static "NEW" text badge on the Store
nav item with a red pinging dot (matching the desktop navbar style). The
dot is conditionally shown based on cosmetics hash changes tracked in
localStorage, and dismissed when the user clicks Store.

- **Help dot on mobile**: Added the yellow help dot (already present on
desktop) to the mobile navbar for consistency, shown for users with
fewer than 10 games played.

### Screenshots:

<img width="1028" height="97" alt="Screenshot 2026-02-21 174029"
src="https://github.com/user-attachments/assets/1ed460dd-4e41-4287-bcb9-73f431e8a953"
/>

<img width="513" height="700" alt="Screenshot 2026-02-21 174333"
src="https://github.com/user-attachments/assets/c6b81296-d36b-424e-9637-e738acd8007a"
/>

## 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-02-21 21:15:36 -06:00
Evan 90978c0e92 bugfix: set lobby start time only when it's the next lobby in rotation (#3261)
## Description:

The master set lobby start times on creation, which caused an issue if
the previous lobby filled up and started before its timer ran out, the
next lobby would have its timer set too far back. For example, if lobby
time is 60 seconds, and the first lobby fills up after 10s, the
subsequent lobby would have its timer set for 110 seconds (60+50).

Instead we have the master set the lobby start time only when it is next
up in rotation. So all lobbies behind it don't have a start time,
because we don't actually know what it should be.

## 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-02-21 21:08:33 -06:00
VariableVince 444aa16ac8 Fix: less console warn spam on each attack-click (#3262)
## Description:

Rabbit suggestion to move console warn for not being able to send boat
to doBoatAttackUnderCursor and remove it from canBoatAttack.

On each attack-click on land, if that land is own land or ally and
canAttack is false, it will check canAutoBoat. Even if user had no
intention to attack, there would be a warning that no boat could be
send. Now only do that warning when user actually intended to send a
boat using hotkey G which calls doBoatAttackUnderCursor.

## 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-02-20 22:58:14 -06:00
Mattia Migliorini 90204f6628 Add alliance renewal action to Radial Menu (#3148)
## Description:

The following PR replaces the (disabled) alliance request button with an
alliance extension/renewal button when the alliance with the target
player is expiring.

Agreeing to renewal via radial menu also hides the message in the
EventsDisplay.

<img width="369" height="364" alt="image"
src="https://github.com/user-attachments/assets/d8040f5c-ad7b-47d0-852f-925ecbf273a8"
/>


https://github.com/user-attachments/assets/aa589edf-6505-46bf-88a3-aa4c2df9137f

Icon size adjusted:

<img width="294" height="252" alt="image"
src="https://github.com/user-attachments/assets/7ca63500-b1fb-427b-965c-cf121a5213da"
/>

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-19 19:47:57 -06:00
VariableVince c235debb57 Cleanup: Replace literals by enums (#3252)
## Description:

Some literals were present that could/should have been enums. Replaced
them.

For Util.ts > createRandomName, also changed type of parameter
playerType from string to PlayerType. All callers already send it this
type.

## 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-02-19 16:58:07 -06:00
VariableVince 7ee07fc9f0 Homepage mobile: put Solo and Ranked buttons above the public lobbies (#3245)
## Description:

Put Solo and Ranked above the public lobby buttons on mobile.

The Solo, Ranked, Create Lobby and Join Lobby buttons fall outside of
view on the average mobile screen. Since Solo is the most played game
mode and could probably be more directed to beginners, this button needs
to be in view for those who don't realize right away that there are more
buttons when scrolling down.

Ranked just has its place to the right of it and moves with it.

Create Lobby and Join Lobby are for players who already know their way
around a bit, so it's ok if they stay at the bottom. This way we
advertise having 3 public lobbies as well, as all 3 are in view always
(the 3rd at least partly which makes one curious to look what lobby it
is showing).

**BEFORE**

https://github.com/user-attachments/assets/c56e124e-069a-48bd-8860-c1113cca102f

**AFTER**

https://github.com/user-attachments/assets/83306828-d1e1-439e-9058-7f741d704ea3

## 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: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
2026-02-19 19:12:59 +00:00
FloPinguin bb1ddfbcaa Make "Not Logged In" button open Account modal & fix Account loading state (#3247)
## Description:

**TerritoryPatternsModal.ts**
- Changed the "Not Logged In" indicator from a static `div` to a
clickable `button`
- Clicking it now closes the skins modal and navigates to the Account
page via `window.showPage("page-account")`
- Added hover effect (`hover:bg-red-500/30`) for visual feedback

**AccountModal.ts**
- Fixed the inline Account modal's loading state ("Fetching account
information...") rendering without a background or header (white text on
light background 💀)
- The loading spinner is now wrapped in `modalContainerClass` (dark
glassmorphic background) with a proper `modalHeader` including the title
and back button, matching the loaded state's appearance

**SinglePlayerModal.ts**
- Changed the "Sign in for achievements" banner from a static `div` to a
clickable `button` that closes the modal and navigates to the Account
page
- Added hover effect for visual feedback

**Matchmaking.ts**
- When the "You must be logged in to play ranked matchmaking" toast
appears, the user is now also navigated to the Account page so they can
log in immediately

## 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-02-19 18:01:27 +00:00
Ryan 70f2abb181 Homepage update & add 3 public lobbies (#3191)
## Description:

Update UI 
check https://homepageupdate.openfront.dev/ 

Improved mobile UI (now fills whole screen for all modals) e.g.:
<img width="432" height="852" alt="image"
src="https://github.com/user-attachments/assets/56de40af-4137-4c57-96b7-3910c9a665b8"
/>

Converted PublicLobby to be "GameModeSelector" to get a nicer 4x4 grid
div, where <GameModeSelector> now handles all the username validation
now (removed redundant code from modals such as matchmaking) also fixed
a bug where someone could have "[XX] X" as thier username (when the
minimum should be 3 chars for their name)

Now visually displays the 3 lobbies ffa/team/special (which is a
continuation from the work done in: #3196 )
<img width="818" height="563" alt="image"
src="https://github.com/user-attachments/assets/a15cd31b-6061-4fb8-83ee-ffde6225cfa7"
/>

updated the background:
<img width="1919" height="807" alt="image"
src="https://github.com/user-attachments/assets/358a7434-51b8-4540-baf2-d1be05053c44"
/>



slightly updated the glassy-look to be less glassy:
<img width="825" height="729" alt="image"
src="https://github.com/user-attachments/assets/1801871b-bbf8-43db-ac53-489337ae80a5"
/>



## 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-02-18 23:11:01 -06:00