Commit Graph

648 Commits

Author SHA1 Message Date
Sky Elder 38cb3027cb Feature Add retaliate keybind (#3801)
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #3174 

## Description:

This PR implements a keybind for retaliation against incoming non-bot
attacks (Players and Nations). It currently will target the most recent
(last in array) incoming attack when the keybind is pressed, choosing
the minimum of attack ratio or incoming attack troop size to retaliate
with. This correlates with the last displayed incoming attack on the
bottom modal. It works when there are multiple attacks (press multiple
times to retaliate against each consecutive attack). If the retaliation
size is smaller than the attack, the keybind can be pressed multiple
times to repeatedly blunt the incoming troop count.

Due to the current keybind logic KeyR cannot be used but Shift+KeyR can
so it is set to that until refactor.

<img width="867" height="121" alt="image"
src="https://github.com/user-attachments/assets/56e80810-4900-4db0-8ce7-1856e13529e5"
/>

No tests have been added as the retaliation feature doesn't seem to have
any to begin with since it is just an attack intent. The keybind is not
configured any differently than others. I did run all current tests and
was all good.

## Please complete the following:

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

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

n1ghtingal3
2026-05-06 12:08:31 -06:00
RickD004 3744027e94 Adds map of the Middle East (#3829)
## Description:

Starting v32 maps right now!

Adds map of the Middle East. Probably one of the most highly requested
maps.

Very large map (3.4M land pixels, similar to Two Lakes, would become the
2nd or 3rd largest map by land area) of the arabian peninsula and
surrounding regions.
This map has both huge areas of land without water access full of desert
terrain, and massive trade chokepoints, which combined will result in
crazy endgames.

The theme of this map are historical, based on the end of WW1.
Historical flags have been added for the nation NPCs to use.

(High map rotation of 8 since this is probably going to become one of
the most popular maps given the relevancy of the region in real life.)

Terrain source from NASA DEM, already credited.


https://github.com/user-attachments/assets/6a1b345f-fd92-42c2-8f92-154fac4c9733

## Please complete the following:

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

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

DISCORD_USERNAME

tri.star1011
2026-05-05 22:12:42 -06:00
Evan f90e73c2f7 conqueror receives 50% of gold when conquering a human player (#3818)
## Description:

The motivation is to prevent snowballing players from also gaining too
much gold by conquering other players

- Adds `conquerGoldAmount` to `Config`/`DefaultConfig`: returns 100% of
captured gold for bots/nations, 50% for human players
- Updates `GameImpl.conquerPlayer` to use this amount for the
conqueror's gold gain (the conquered player still loses their full gold)

## Please complete the following:

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

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

evan
2026-05-02 09:23:49 -06:00
babyboucher 4f20d2b332 TypeScript update to 6.0.3 (#3806)
## Description:

Updating TypeScript to 6.0.3.
Updating TypeScript-eslint to 8.59.1 for TS6 support.
Concurrently needed to get updated as well to remove deprecated warning.

Most things deleted are now just defaults.

## Please complete the following:

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

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

Babyboucher
2026-04-30 15:49:24 -06:00
Zixer1 742a544a69 2661 PR 3/3 Warship Manual Override, Aggro Override, and Heal-at-Port Command (#3501)
Part of [#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661)
(split into 3 PRs so they are not too large..)

## Description:

Part 3/3 of
[#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661).

This PR adds the retreat control and override behavior for warships:

- Manual override: moving a warship manually cancels retreat and
suppresses auto-retreat for 5 seconds
- Aggro override: a retreating warship will aggro a nearby enemy
transport or warship before continuing retreat
- Heal-at-port command for sending a warship to a friendly port manually
- Friendly-port validation for HealAtPortExecution
- Regression tests for manual override, aggro override, and heal-at-port
behavior



## Please complete the following:

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

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

zixer._

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
Co-authored-by: evanpelle <evanpelle@gmail.com>
2026-04-30 13:54:28 -06:00
evanpelle 3476bfc674 Changed the default in UserSettings.ts:386 from 1 to 0. New users (or anyone who hasn't touched the slider) will now start with sound effects muted. 2026-04-28 21:39:31 -06:00
RickD004 a989bcbeb0 Rename Mediterranean to Mare Nostrum 🏛️ (#3768)
## Description:

Change inspired by Territorial.io . Renames the Mediterranean sea map to
Mare Nostrum, the ancient name of the sea given by the Romans. The NPCs
of the map are already roman provinces, so this change is more thematic
and also adds a creative name like "Gateway to the Atlantic" and
"Between Two Seas"

## 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
2026-04-28 19:15:29 -06:00
FloPinguin 7654537a00 Much better river handling for nations and tribes! 🏞️ (#3786)
## Description:

In this example, the two nations DONT see each other as neighbors, but
as ISLANDERS. Because they dont have a direct border connection, there
is water in between.

<img width="526" height="329" alt="image"
src="https://github.com/user-attachments/assets/cf2c15b5-7793-4445-afd2-920d6cd50a2a"
/>

This is a big problem, because most of the logic in AiAttackBehavior
gets ignored. Only the "islander" strategy runs (late, because its a
not-important strat).

### Summary

- `PlayerImpl.neighbors()` now includes cross-water neighbors: a new
`shoreReachableNeighbors()` helper samples every 10th shore border tile
and looks up to 5 tiles in each cardinal direction across water, finding
land owners on the other side (covers rivers up to 4 tiles wide).

- `AiAttackBehavior.maybeAttack()` extends the `hasNonNukedTerraNullius`
check to also trigger on TN detected via `player.neighbors()`, so
nations notice and pursue TN that is only reachable across a river.

- `sendAttack()` uses a new `hasLandBorderWithTerraNullius()` land-only
adjacency check to decide between a land attack and a boat attack for
TN, rather than `sharesBorderWith()` which includes water tiles.

- Added `sendBoatAttackToNearbyTerraNullius()`: when no TN land is
directly adjacent, the AI scans its shore border tiles for unowned land
across water and dispatches a transport ship.

### Also works for Tribes!

Tribes can boat rivers now, really cool.


https://github.com/user-attachments/assets/382e85aa-c437-4e0c-afc2-0c381432da3d

## 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-04-28 18:25:11 -06:00
Patrick Plays Badly 79ac66df3c Add map los angeles (with roads) (#3708)
## Description:

Add Map - Los Angeles (With Roads)
Whats NY without LA? What is LA without its roads? Here is a larger map
where I have turned LAs road network into rivers. Nations named to areas
of the city or surrounding area. Map geo data is from OpenTopography. I
stenciled the highways from screenshots of online maps and some artistic
choice.


https://discord.com/channels/1284581928254701718/1482605671697613081/1482605671697613081
https://www.youtube.com/watch?v=6td9wiQKE9s

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

##Discord username
PlaysBadly

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
2026-04-27 22:05:17 -06:00
Zixer1 c0febacb8e 2661 PR 2/3 Warship Port Healing, Docking Capacity, and Waiting Behavior (#3499)
Part of [#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661)
(split into 3 PRs so they are not too large..)

## Description:

Part 2/3 of
[#2661](https://github.com/openfrontio/OpenFrontIO/issues/2661).

This PR adds port-based healing and docking behavior:
- Passive healing near friendly ports
- Active docked healing pool scaled by port level and shared across
docked ships
- Docking radius and capacity-by-port-level behavior
- Waiting behavior near full ports until a slot opens
- Auto-undock once fully healed

For the active healing, it works like `ActiveHeal = (PortLevel * 5) /
DockedShipsAtThatPort`
Ex:
1 ship at level 1 port -> +5 HP/tick
1 ship at level 2 port → +10 HP/tick
2 ships at level 3 port → +7.5 HP/tick each

Includes regression tests covering healing math and docking/waiting
behavior.

## Please complete the following:

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

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

zixer._
2026-04-26 21:42:13 +00:00
RickD004 32a254b375 add Baja California map 🏜️ (#3759)
## Description:

Adds map of the Baja California peninsula in Mexico. Majority of this
map is brown terrain to simulate the (Sonora) desert
Mexican and American states as NPCs. The peninsula while having little
landmass is balanced by the fact it will be very easy to pirate from it

<img width="503" height="583" alt="Captura de pantalla 2026-04-18
151337"
src="https://github.com/user-attachments/assets/b1acd93a-fbe4-4d17-95b6-5f4c86ec872f"
/>

Relief data from OpenTopography, already credited

## 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
2026-04-25 15:27:30 -06:00
Zixer1 37079e6a05 2661 PR 1/3 Warship Retreat Core, Blue UI Signal, and Transport-First Target Priority (#3498)
Part of #2661 (split into 3 PRs so they are not too large..)

## Description:

Part 1/3 of #2661.

This PR adds warship retreat basics, a blue retreating UI state, and
updates target priority.

Added:
- Retreat state handling
- Blue visual for retreating warships
- Target priority: transport > warship > trade 
- Tests for retreat and target priority

Example video:
https://youtu.be/2hE2qeOeY48
Ship retreating:
<img width="630" height="488" alt="image"
src="https://github.com/user-attachments/assets/56d3e6d5-08af-453d-afe5-ee21dd6f3414"
/>
Ship healing:
<img width="483" height="311" alt="image"
src="https://github.com/user-attachments/assets/aeaf2239-bb81-444f-84ef-62dbcb48fddf"
/>
Back to being deployed:
<img width="585" height="358" alt="image"
src="https://github.com/user-attachments/assets/875828a2-8a24-4593-ac76-26426bb81057"
/>

## Please complete the following:

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

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

zixer._
2026-04-24 08:26:14 -06:00
RickD004 aa70d6742a add Archipelago Sea map (#3749)
## Description:

Adds Archipelago Sea map. The island map to end all island maps.

This is a real location in Southwest Finland, famous for being the
archipelago with the most islands in the world.

This map is made for the very loud playerbase who enjoys naval maps.
This is probably the most heavy naval map we are gonna get. Given this
is a very experimental and unorthodox map like amazon or 4islands, it
has a rotation of 3 , low but still should pop in for people who prefer
island maps.


https://github.com/user-attachments/assets/19b32a4f-825b-4b82-aec3-b24c387b9cdd

## 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
2026-04-24 08:07:48 -06:00
RickD004 ec6e14051a Add an Antarctica map 🐧 (#3740)
## Description:

(re-submission of older PR because the old one had too many merge
conflicts)

Re-adds and reworks the previously April Fools "Reglaciated Antarctica"
as a new map, this time with proper elevation data: Map of Antarctica
centered in the South Pole.

The "appeal" of this a map is that it has no green terrain. (As such it
has a really low rotation number (of 1). )

This also completes the Continental map category (antarctica was the
only continent out of the traditional 7 missing).


https://github.com/user-attachments/assets/28302464-c533-483e-8a1b-2699093921ff

The base map image is a composite of 2 relief maps from 2 different
sources, both properly credited

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

Co-authored-by: iamlewis <lewismmmm@gmail.com>
2026-04-22 16:04:56 -07:00
Jarifa 0801798fbd Feat: Alliance and betrayal hotkeys (#3110)
Original Feature request by @FloPinguin  
Resolves #3077 

## Description:

Adds hotkeys for Requesting alliances and breaking alliances. This
allows for players to send or break alliances whose tile is under the
cursor, without opening the radial menu.

Keybinds:
New "Ally Keybinds" section in Settings -> Keybinds
Request alliance: Default: K - sends an alliance request to the
player/bot/nation under the cursor
Break alliance: Default: L - breaks the alliance with the player at the
cursor

Behavior:
- Cursor must be over a tile owned by the target player. The action runs
only when the game allows it, following the same logic as the radial
menu. (canSendAllianceRequest and canBreakAlliance)
- When an alliance request is sent, the events log shows: "Alliance
request sent to [target]" for confirmation. No extra message for
breaking an alliance (betrayal/debuff message already exists and is sent
upon breaking an alliance)

## Screenshots:
Keybind menu:
<img width="739" height="595" alt="image"
src="https://github.com/user-attachments/assets/ee958eab-fd50-4971-85c5-dfd49c6f0bdc"
/>
In game logs:
<img width="373" height="232" alt="image"
src="https://github.com/user-attachments/assets/2cf6bb07-5f0d-425a-82d3-65a44fef99c5"
/>

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


Discord username:
_Dave9595_
2026-04-21 14:34:51 -07:00
Ivan Batsulin 29a1e8dfda feat: multi-warship selection with Shift+drag box (#3677)
Resolves #3666

## Description:

Adds RTS-style box selection for warships. Hold Shift and drag (desktop)
or long-press and drag (touch/mobile) to draw a selection rectangle —
all player-owned warships inside get selected at once. A subsequent
click/tap on water sends them all to that location.

- `SelectionBoxLayer` — pixel-dashed rectangle in world-space, player
territory color; shared between desktop and touch
- `UILayer` — same pulsing selection outline on each box-selected
warship; clears correctly when switching between single/multi selection
- `UnitLayer` — finds warships in screen rect, filters inactive ships
before sending; touch support included
- `InputHandler` — Shift+drag and touch long-press+drag both emit
selection box events; cursor becomes crosshair on Shift; discards active
ghost structure on Shift press; configurable via `shiftKey` keybind
- `Transport` — single atomic `move_multiple_warships` intent (no split
on socket drop)
- `Schemas` + `ExecutionManager` + `MoveMultipleWarshipsExecution` —
server fans out atomic intent into individual `MoveWarshipExecution` per
ship
- `DynamicUILayer` — `MoveIndicatorUI` chevron animation on target tile
for both single and multi move
- `UnitDisplay` — warship tooltip Shift hint via `translateText`
- `HelpModal` — new hotkey row: Shift + drag → select multiple warships

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

## UI update
### Mouse + Keyboard


https://github.com/user-attachments/assets/3f35ab5e-1f3c-4c5d-bc4f-aabccf64dc60

### Touch


https://github.com/user-attachments/assets/0d6aec3f-44fa-4fee-b5c6-b267b9b14d79

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

fghjk_60845
2026-04-21 14:06:07 -07:00
RickD004 ec8884410c Add new Bering Sea map, reduce Strait rotation (#3706)
## Description:

Re-submission of older PR:
https://github.com/openfrontio/OpenFrontIO/pull/3617
<img width="612" height="396" alt="image"
src="https://github.com/user-attachments/assets/955a0b2e-1df9-4fa3-a389-235f46a90f69"
/>

Regarding Bering Sea and Bering strait maps:
<img width="576" height="87" alt="image"
src="https://github.com/user-attachments/assets/f57031eb-f9c0-4e1e-83ff-204df51bb6c2"
/>
<img width="589" height="93" alt="image"
src="https://github.com/user-attachments/assets/629e71ba-489f-41cf-b67e-2476af8704f2"
/>

Because of mixed opinions about replacing the maps, i propose at least
this temporary compromise: Bering Sea gets added as a new map and Bering
Strait gets its rotation lowered to a small number. Since the classic
maps are probably being taken off rotation, we should have some small
breathing space for the 2 maps.

In the near future we should discuss between mapmakers in the dev server
whether we keep, replace or completely take Bering Strait off rotation,
and/or a community poll.

## 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
2026-04-17 17:36:38 -07:00
FloPinguin bcd9cd6af5 Cache shared-water computation for nation port placement 💧 (#3696)
## Description:

- Cache `sharedWaterComponents` globally with a 30-tick (~3s) TTL so all
nations share one `O(total_border_tiles)` pass instead of each nation
re-scanning every other player's border on every call.
- Treat ocean as always-shared: any ocean neighbor short-circuits as a
valid port site, skipping the `getWaterComponent` lookup in both the
build pass and the per-tile port check.
- Exclude bots and mutually-embargoed players from the trade-partner
candidate set, so nations no longer avoid port sites that only "share"
water with a player they can never trade with.

Port placement is not time-critical, so the 3-second staleness is
acceptable and lets the expensive build amortize across many attack
cycles.

### Performance
Benchmarked on World map (2000×1000, 61 nations) with the realistic call
pattern of ~3 nations invoking `sharedWaterComponents` per tick:

- **Before (main):** ~414 μs per tick
- **After:** ~8 μs per tick amortized (29/30 ticks hit the warm cache;
1/30 rebuilds)
- **~50× faster** on this AI hot path

## 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-04-17 17:34:45 -07:00
FloPinguin 0801cad0b5 Fix NaN coordinates in Warship patrol logic 🚢 (#3697)
## Description:

This PR fixes the `Invalid coordinates: NaN,NaN` crash during Warship
patrol execution.

### Root cause

`WarshipExecution.randomTile` picks a patrol destination inside
`warshipPatrolRange / 2` of the current patrol tile. When a search fails
to find a valid tile, the range expands by 50% per retry (`100 → 150 →
225 → 337`) and becomes odd. Once odd, `warshipPatrolRange / 2` is a
float (e.g. `112.5`), which is handed straight to
`PseudoRandom.nextInt`:

```ts
Math.floor(this.rng() * (max - min)) + min;
```

With a float `min`, this returns `integer + float` - a float. Despite
its name, `nextInt` was silently returning a non-integer. From there:

- `x = mg.x(patrolTile) + floatOffset` → float
- `mg.isValidCoord(floatX, floatY)` → `true` (only bounds were checked)
- `mg.ref(floatX, floatY)` → `yToRef[floatY] + floatX` → `undefined +
float` → `NaN`
- `hasWaterComponent(NaN, …)` → `miniMap.ref(NaN, NaN)` → **throw**

### Why this only started crashing recently

The float‑leaking `nextInt` bug has been latent since at least the
pathfinding refactor (#2866, January), which introduced the
`hasWaterComponent` check. It was invisible because the guard directly
above it short‑circuited on `NaN`:

```ts
if (!this.mg.isOcean(tile) || (!allowShoreline && this.mg.isShoreline(tile))) continue;
```

For a `NaN` tile ref, `terrain[NaN]` is `undefined`, so:

- `isOcean(NaN)` → `Boolean(undefined & OCEAN_BIT)` → **`false`**
- `isLand(NaN)` → **`false`**
- `isWater(NaN)` → `!isLand(NaN)` → **`true`**

Before: `!isOcean(NaN)` was `true`, execution hit `continue`, and the
poisoned ref never reached `hasWaterComponent`. The "Trading in lakes"
PR (#3653) relaxed that single line to allow patrol on lakes:

```diff
- if (!this.mg.isOcean(tile) || ...) continue;
+ if (!this.mg.isWater(tile) || ...) continue;
```

Because `isWater(NaN)` is `true`, `!isWater(NaN)` is now `false` -
execution falls through to `hasWaterComponent(NaN, …)` and crashes.
#3653 didn't introduce the bug; it just happened to remove the
accidental NaN filter that was hiding it.

### Changes

- **`PseudoRandom.nextInt`** - root‑cause fix. Floors both `min` and
`max` so `nextInt` always returns an integer regardless of what callers
pass. Future callers can't re‑trip this trap.
- **`WarshipExecution.randomTile`** - replaced the unsafe
`this.warship.patrolTile()!` non‑null assertion with a proper
`undefined` guard that returns early.
- **`GameMap.isValidCoord`** - defense in depth: also requires
`Number.isInteger(x)` and `Number.isInteger(y)`. Non‑integer coords can
still be produced outside `nextInt` (trig, arithmetic); this makes
`ref()` fail loudly at the boundary instead of silently producing `NaN`
refs.

### Original stacktrace

Please paste the following in your bug report in Discord:
Game crashed!
game id: gGicMpDh
client id: wXE5SpT2
Error: Invalid coordinates: NaN,NaN
Message: Error: Invalid coordinates: NaN,NaN
at at.ref
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:64853)
at r_.hasWaterComponent
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:243326)
at l_.hasWaterComponent
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:260740)
at b1.randomTile
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:92634)
at b1.patrol
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:91728)
at b1.tick
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:89996)
at
https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:251463
    at Array.forEach (<anonymous>)
at l_.executeNextTick
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:251383)
at p_.executeNextTick
(https://nightly.openfront.dev/assets/Worker.worker-DL_guV2P.js:31:271256)

Discord:
https://discord.com/channels/1284581928254701718/1494336024740888667

## 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-04-17 17:34:12 -07:00
FloPinguin ae96eb7e98 Fix nation nuke crash when attacker has no remaining tiles 🛡️ (#3703)
## Description:

Fixes a game crash (`Error: array must not be empty`) thrown from
`PseudoRandom.randElement` when a nation tries to pick a nuke target
whose territory no longer exists.

## Root cause

`NationNukeBehavior.findBestNukeTarget` calls
`findIncomingAttackPlayer`, which iterates the player's
`_incomingAttacks`. `AttackImpl` instances can linger in this array past
the point where the attacker has lost all tiles — the attack is only
removed on explicit `delete()` and `removeOnDeath` cleanup isn't
guaranteed to run before other executions tick within the same turn. A
dead attacker gets returned as the nuke target, `randTerritoryTileArray`
samples their (empty) territory, and `randElement` throws on the empty
array.

## Fix

- `Player.incomingAttacks()` now filters out attacks whose attacker is
no longer alive, so consumers can't observe stale references from
mid-tick deaths.
- `randTerritoryTile` guards against `numTilesOwned() === 0` before
falling back to `randElement(p.tiles())` as a defense-in-depth safeguard
at the util level.
- `PlayerImpl.toUpdate()` now uses the `incomingAttacks()` /
`outgoingAttacks()` accessors (rather than the raw `_` arrays) so
serialized client state stays consistent with the server-side view.

`outgoingAttacks()` is intentionally left unfiltered, the engine relies
on seeing in-flight attacks during their retreat phase after a target is
conquered (attack merging/cancellation in `AttackExecution.init`).

### Bug report from discord

Please paste the following in your bug report in Discord:
Game crashed!
game id: LQDSWbh6
client id: JjwysSLN
Error: array must not be empty
Message: Error: array must not be empty
at sa.randElement
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:28:39166)
at H1
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:116429)
at Jc
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:116135)
at G1.maybeSendNuke
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:117597)
at n5.tick
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:171764)
at https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:251463
    at Array.forEach (<anonymous>)
at c.executeNextTick
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:251383)
at b.executeNextTick
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:271256)
at S_
(https://main.openfront.dev/assets/Worker.worker-DoPM94lr.js:31:366356)

## 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-04-17 17:32:49 -07:00
FloPinguin f1f63ec9b4 Partial Pathfinding Rebuild 💧 (#3689)
## Description:

Another big water nukes performance improvement.

Performance measurements (water-nuke-detonation on GWM):

<img width="203" height="103" alt="image"
src="https://github.com/user-attachments/assets/b3d62575-d4bd-43c9-a5af-af127d73a9c5"
/>

I did a lot of testing with throwing water nukes and sending boats in
the area, paths are looking clean & correct!

## 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-04-15 16:17:40 -07:00
FloPinguin 9821e8e041 Add host cheats for streamers (Specifically Enzo) (#3671)
## Description:

- Adds a "Host Cheats" toggle in the private lobby options section that
reveals a dedicated section with four host-only cheats: infinite gold,
infinite troops, gold multiplier, and starting gold
- Only the lobby creator receives the cheat effects in-game (checked via
`isLobbyCreator` in DefaultConfig)
- Joining players see active host cheats displayed as yellow badges in
the lobby UI
- Adds `hostCheats` optional object to `GameConfigSchema` and wires it
through the server config update whitelist
- Raises the intent size limit for `update_game_config` messages
(lobby-only, not stored in turn history) to prevent rate-limiter kicks
(I always got too-much-data-kicked after selecting "host cheats" lol)

<img width="861" height="525" alt="image"
src="https://github.com/user-attachments/assets/51e51ec4-c2e8-46ca-b258-11a93487964f"
/>


<img width="933" height="825" alt="image"
src="https://github.com/user-attachments/assets/5acbd38d-2097-42e1-ba78-0fb17d6afe82"
/>

## Please complete the following:

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

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

FloPinguin
2026-04-15 15:20:08 -07:00
Aotumuri d05f5e748c Fix external train trade attribution in match results (#3589)
## Description:

A player reported that income received from opponents' trains was
missing from match result logs.
Fix external train trade attribution to the station owner and add
regression tests for train income stats.

## 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-04-15 02:35:32 +00:00
FloPinguin 35a64fa0d9 Big Water-Nukes Performance Improvements 💧 (#3668)
## Description:

### 1. Water Magnitude Calculation Optimization (WaterManager.ts)
* Boxed BFS Approach: Refactored the water magnitude recomputation to
use "Dirty" and "Seed" boxes. Instead of a global update, the system now
only recalculates magnitudes within a specific radius of the affected
area, significantly reducing CPU load after water-nuke-explosions
* Shoreline Bit Optimization: Narrowed the scope for updating shoreline
bits to a 2-ring neighborhood around converted tiles, avoiding
unnecessary checks across the entire map.

Performance test on the world map:
- AtomBomb (r=30): 24ms (was 344ms with global BFS), 2,993 changed tiles
(was 630k)
- Massive (r=200): 178ms (was 378ms), 130k changed tiles (was 654k)

### 2. Pathfinding Rebuild Staggering (PathFinder.ts,
TradeShipExecution.ts, TransportShipExecution.ts)
* Distributed Rebuilds: Introduced a staggering mechanism in
WaterPathFinder. Ship pathfinders now wait a randomized/distributed
number of ticks (0 - 5 seconds) before rebuilding after a water graph
change.
* CPU Spike Mitigation: By spreading out these expensive A* rebuilds
over time, we prevent lag when hundreds of ships attempt to re-path
simultaneously
* Like Mole said it: "Pretty realistic I;d say the capitan needs a
second to realize the big nuke on the left opened a new path"

From a performance test on the big new Luna map:
Graph rebuild: 256.4ms
Pathfinder-Rebuild of 329 ships (Including other Executions): 1564.4ms
(No longer noticeable, spread over 5s)

###  3. Performance Refinements
* Simplified deep ocean magnitude logic within the optimized BFS flow.
   * Improved memory efficiency by utilizing clipped BFS wavefronts.

## 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-04-14 16:17:15 -07:00
RickD004 897a3a3566 adds map of the Caucasus (#3661)
## Description:

Adds map of the Caucasus. This map is made especifically because of the
new update that allows ports to be placed on smaller bodies of water,
rather than just the largest one.

This map has 2 disconnected, large bodies of water of similar size
(Black and Caspian seas), which would create 2 independent trade
systems, creating a more dynamic economy.

<img width="667" height="541" alt="Captura de pantalla 2026-04-13
195810"
src="https://github.com/user-attachments/assets/d980b831-d920-4a2e-9d92-40a7be96ead6"
/>
<img width="539" height="439" alt="Captura de pantalla 2026-04-13
200005"
src="https://github.com/user-attachments/assets/e973a66a-8796-40dc-b8b3-39b49f687609"
/>

## 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
2026-04-13 20:02:11 -07:00
VariableVince 318d1e2c44 Refactor/Fix(UserSettings): last localstorage calls now use UserSettings, and Keybinds fixes (#3619)
## Description:

1) Have last localstorage calls for keybinds and attack ratio also use
UserSettings cache instead, after #3481. Remaining calls to localstorage
are for different things than user settings, so they are left as is.

2) Consolidate and centralize keybinds logic. And three fixes for it.

- **UnitDisplay** and **UserSettingsModal**: _parsedUserKeybinds_ is
introduced in **UserSettings** to centralize their logic. It is also
used by _normalizedUserKeybinds_, see point below.

- **UserSettingsModal**
-- replaced unwanted cast `as SettingKeybind` by a typed QuerySelector.
-- renamed this.keybinds to this.userKeybinds for more clarity, and
distinction from defaultKeybinds.
-- state private _userKeybinds_: remove type string[] since
loadKeybindsFromStorage replaces a value array by its first string
element, so it can not contain string[] anymore.
-- _handleKeybindChange_ and _getKeyValue_: no need to check for
Array.isArray anymore, see above reason.
-- **Fix**: checks after calling _parsedUserKeybinds_ are improved a
bit: don't delete all keybinds and print a console warning when finding
just one invalid keybind and (i think i have seen people complaining
about things being removed). Instead it now migrates or throws out the
invalid ones but keeps the valid ones. Also works with the "Null" value
expected and removed within
**UserSettingsModal**._handleyKeybindChange_() and in **HelpModal**.
When legacy value is an array and key is empty, don't put value as key
but get first array element or empty string as key name. So that check
on line 68 is true.

- **HelpModal** and **InputHandler**: Also centralize/consolidate their
logic more, by having __keybinds()_ from **UserSettings** perform
fetching _getDefaultKeybinds_ and _normalizedUserKeybinds_.
-- Functionality in _normalizedUserKeybinds_ is the same: Where
HelpModal did return [k, v.value] if typeof (v as any).value ===
"string", this is now handled by lines 309-310 of normalizedKeybinds
still the same but with less lines. Same for old HelplModal if (typeof v
=== "string") return [k, v], this is stil returned by line 112 of
normalizedKeybinds. And return [k, undefined] when (typeof val !==
"string") as was done in InputHandler, isn't needed as values that
weren't strings were already filtered out right after which we still do
on line 314 of normalizedKeybinds.
-- **Fix** in _normalizedUserKeybinds_: added one extra thing that was a
discrepancy between **HelpModal**/**InputHandler** and
**UserSettingsModal** before: **UserSettingsModal** would handle array
values, and normalize them by picking only the first value if it is a
string. Now have _normalizedKeybinds_ do the same. Otherwise it would
have thrown those values out while **UserSettingsModal** would have kept
the first value. This may still help a returning player who hasn't
played in the last version (i think i have seen people complaining about
things being removed, but that may not have been about this). And makes
the logic more consistent between **UserSettingsModal** and
**HelpModal**/**InputHandler**.

- **UserSettings**: 
-- _getDefaultKeybinds_: centralized/consolidated logic, accepts
Platform.isMac parameter. In **HelpModal**, **InputHandler** and
**UserSettingsModal** the same list with default keybinds was hardcoded.
Now they all read from _getDefaultKeybinds_. The list of default
keybinds in **HelpModal** was a little shorter, but that doesn't matter
since its _render_() function has hardcoded which of the hotkeys
**HelpModal** shows. Have thought about putting default keybinds in
**DefaultConfig** but with all the logic handled through
**UserSettings**, this seemed the better place in the current refactor.
-- _removeCached_: make public, now that **InputHandler.test.ts** needs
to be able to call it. We could instead make a public function like
removeKeybinds() and keep removeCached() private, but went with this for
now.
-- _parsedUserKeybinds_: centralized/consolidated logic for
**UserSettingsModal**/**UserDisplay**. Always returns an object, even an
empty one if the JSON wasn't parsable.
-- _normalizedKeybinds_: centralized/consolidated logic. Used by
_keybinds_() which is now called by **HelpModal**/**InputHandler**.
-- _keybinds_: now uses getDefaultKeybinds() and normalizedKeybinds() to
get the default and user changed keybinds.
-- **Fix** in _keybinds_: it now removes a key if it is Unbound by the
user in **UserSettingsModal**. Instead of first loading the
parsedUserKeybinds, removing "Null" keys from it, and then merging that
with defaultKeybinds (so default key would overwrite an unbound key), we
now merge parsedUserKeybinds with defaultKeybinds and after that remove
"Null" keys from it (so that unbound key stays removed).
For example if Boat Attack Up is set to "None" ("Null") by clicking
Unbind, there is now no hotkey working for it anymore. Even when the
default is "B".
Why? This prevents the user from being confused, they have deliberately
Unbound it, they don't understand why it still works (have seen bug
reports and game feedback about this)? Also more importantly: they used
to now be able to bind "B" to another action. Effectively making key "B"
bound to two actions: the user choosen one and Boat Attack. This also
makes the logic more consistent. Because building hotkeys in
**UnitDisplay** already didn't work when unbound, eg. when Build Missile
Silo was Unbound, the "5" key did not do anything anymore (there is a
fallback in **UnitDisplay** in case the key is actually null, but it
does respect "Null" as it should).
-- _setKeybinds_: have it accept an object, it stringifies it itself.
Callers UserSettingsModal and InputHandler.test.ts now just send either
a string or an object.

- **InputHandler.test.ts**: 
-- use **UserSettings** functions instead of localStorage for more
real-world testing.
-- change test "ignores non-string values and preserves defaults,
removes 'Null' for unbound keys". As explained above, as a fix we no
longer preserve unbound ("Null") keys within InputHandler.
UserSettings.keybinds() now removes "Null" keys as explained above.

- ControlPanel: use UserSettings to fetch initial attack ratio.

## 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-04-13 15:56:32 -07:00
Patrick Plays Badly 3704914676 Add map conakry (#3656)
## Description:

Add Map - Conakry

Dataset from OpenTopography. Map rotated 45 degrees from north. This is
a 'long' map in a similar catagory as Amazon River with public rotation
adjusted to match. Different than Amazon in that its stubbier, one
sided, and has various terrain obstacles. Also its a really cool looking
piece of geography.

https://www.youtube.com/watch?v=OsMDbnnOOko


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

I removed an additional bot from the far left to help balance the map.
Some rivers were extended past realism to help section off the map in
areas. Size of map kept below average intentionally.


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

PlaysBadly
2026-04-12 19:22:33 -07:00
FloPinguin 17c1a6300f Trading in lakes 🚤 (#3653)
## Description:

- Widened port placement and warship spawn/patrol checks from
`isOcean`/`isOceanShore` to `isWater`/`isShore`, so ports can be built
on lake shores and ships can operate on lakes, we discussed it here:

<img width="996" height="423" alt="image"
src="https://github.com/user-attachments/assets/acf1e970-9631-4848-a0ed-6d0470616e1d"
/>

- Filtered `tradingPorts()` by water component so ports only attempt
trades with reachable ports - prevents silent path-not-found failures
across disconnected water bodies
- Applied the same water component filter when a captured trade ship
reroutes to its new owner's nearest port
- Removed the `WaterManager` fallback that force-marked isolated
water-nuked-tiles as ocean (no longer needed since lakes are now
navigable)
- Added a check to prevent nations from building ports on water bodies
that aren't accessible to other players

## 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: Evan <evanpelle@gmail.com>
2026-04-12 17:18:52 -07:00
Patrick Plays Badly 51716e9dc4 Add map luna (the moon) (#3635)
## Description:
Add map Luna (The Moon).

https://youtu.be/6L6vS9VvD8k


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

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

##  Discord username 
PlaysBadly

---------

Co-authored-by: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
2026-04-11 13:17:43 -07:00
VariableVince f0b3c490b1 Fix: flag not visually removed when selecting default/none flag + should not update flag on clearFlag (#3626)
## Description:

https://github.com/openfrontio/OpenFrontIO/pull/3479 put setFlag and
clearFlag in UserSettings.
https://github.com/openfrontio/OpenFrontIO/pull/3481 did some updates to
incorporate them into UserSettings cache. But made two misjudgments.

Which led to: when default/none flag was selected, FlagInput would still
display the previously selected flag. And on clearFlag call, unnecessary
updates where send to FlagInput > updateFlag.

This PR fixes it.

- Don't send update event on clearFlag by default, just like PR 3479
didn't. Although i could imagine potential cases where we would want to
update the displayed flag in FlagInput. Not when clearFlag is called
from Auth > logOut. But maybe in some, but not all, cases when it is
called from Cosmetics > getPlayerCosmeticsRefs. That is for future
investigations by another contributor.

- Do sent update event when clearFlag is called from setFlag, if the
"country:xx" (default/none) flag is set. Previously, it would have sent
event this.emitChange(FLAG_KEY, **"country:xx"**). Now, via clearFlag,
it sends this.emitChange(FLAG_KEY, **null**)

- Have FlagInput > updateFlag handle null. Previously it expected to
always recieve a string, even for default/none flag "country:xx". Now it
will also set this.flag to null if it recieves an event with null. It
being able to handle event value null actually hardens the code so seems
better to me either way. FlagInput > isDefaultFlagValue() already did
handle null values to determine the default flag, so no changes needed
there.

## 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-04-09 19:49:27 -07:00
FloPinguin 7f7cbba12f Water-Nukes 💧 (#3604)
## Description:

Adds a new `waterNukes` game config option that causes nuclear
detonations to convert land tiles into water instead of just leaving
fallout. When enabled, nuked land tiles are batched and converted to
water each tick, with full terrain metadata updates including:

- Ocean bit propagation from adjacent ocean tiles (BFS flood fill)
- Magnitude recomputation via BFS from remaining coastlines
- Shoreline bit fix-up in a 2-ring neighborhood around converted tiles
- Minimap terrain sync (majority-rule downsampling)
- Throttled water navigation graph rebuild (every 20 ticks) for ship
pathfinding
- Ship executions detect graph rebuilds and refresh their pathfinders
- TransportShips auto-retreat if their destination becomes water
- Water nuke craters use a smoothed angular noise ring with a
bounding-box scan instead of the regular per-tile random coin flip with
BFS, producing clean blob-shaped craters without scattered land pixels
that players would have to boat to individually

The `TerrainLayer` now incrementally repaints tiles that changed terrain
type, and tile update packets encode the terrain byte alongside tile
state so clients can reflect water conversions in real time.

When `waterNukes` is disabled, behavior is unchanged (fallout only).

Includes a new test suite (WaterNukes.test.ts) covering the conversion
pipeline, ocean propagation, magnitude recalculation, shoreline updates,
and minimap sync.

Also adds a new public game modifier for the special rotation.

### The only problem
A bit of lag on impact. But otherwise it works great and is fun. Maybe
needs some followup improvements if it gets merged.
I think its very cool in baikal / four islands team games. Chip away the
territory of your opponents.
Its also fun to turn The Box / Alps into a water map (its actually
possible to boat-trade then)

### Media

Video does not show the updated craters


https://github.com/user-attachments/assets/aed8bf08-0e94-4484-b997-4de11ae313d9

Updated craters (no tiny islands after impact):

<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/e896870b-bc9d-493d-8bc8-b3a5427d69d3"
/>

<img width="1472" height="920" alt="image"
src="https://github.com/user-attachments/assets/677065aa-0159-48cd-af44-a91b0f57adfc"
/>

<img width="1296" height="892" alt="image"
src="https://github.com/user-attachments/assets/886ffaba-541f-4e46-97c6-ce963f632fe0"
/>

## Please complete the following:

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

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

FloPinguin
2026-04-08 20:56:02 -07:00
babyboucher f3cbca059f Remove dead color code (#3613)
## Description:

Remove dead code relating to colors.

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

babyboucher
2026-04-08 14:52:12 -07:00
Loncile 38222bfe34 Strait of Malacca map adition (#3599)
## Description:
Adition of Strait of Malacca map

It adds a new map, requested by
Coolson
awildcoolson
Describe the PR.
The map added is a map inspired by the strait of Malacca, it contains
two large landmasses and some islands
## 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:

lerithmercano

---------

Co-authored-by: Ricky G.P. <realtacoco@gmail.com>
Co-authored-by: VariableVince <24507472+VariableVince@users.noreply.github.com>
2026-04-07 22:12:12 -07:00
VariableVince 341f344ce5 Perf/Refactor(UserSettings): caching makes it 10-20x faster (#3481)
## Description:

Skip slow and blocking LocalStorage reads, replace by a Map. Also some
refactoring.

### Contains

- No out-of-sync issue between main and worker thread: Earlier PRs got a
comment from evan about main & worker.worker thread having their own
version of usersettings and possibly getting out-of-sync (see
https://github.com/openfrontio/OpenFrontIO/pull/760#pullrequestreview-2845155737,
https://github.com/openfrontio/OpenFrontIO/pull/896#pullrequestreview-2871836979
and https://github.com/openfrontio/OpenFrontIO/pull/1266.
But userSettings is not used in files ran by worker.worker, not even 10
months after evan's first comment about it. In GameRunner,
createGameRunner sends NULL to getConfig as argument for userSettings.
And DefaultConfig guards against userSettings being null by throwing an
error, but it has never been thrown which points to worker.worker thread
not using userSettings. So we do not need to worry about syncing between
the threads currently.
(If needed in the future after all, we could quite easily sync it, by
loading the userSettings cache on worker.worker and listening to the
"user-settings-changed" event @scamiv to keep it synced (changes in
WorkerMessages and WorkerClient etc would be needed to handle this).

- Went with cache in UserSettings, not with listening to
"user-settings-changed" event: "user-settings-changed" was added by
@scamiv and is used in PerformanceOverlay. Which is great for single
files that need the very best performance. But having to add that same
system to any file reading settings, scales poorly and would lead to
messy code. Also, a developer could make the mistake of not listening to
the event and it would end up just reading LocalStorage again just like
now. Also a developer might forget removing the listener or so etc. The
cache is a central solution and fast, without changes to other files
needed and future-proof.

- Make sure each setting is cached: UserSettingsModal was using
LocalStorage directly by itself for some things. Made it use the central
UserSettings methods instead so we avoid LocalStorage reads as much as
possible. For this, changed get() and set() in UserSettings to getBool()
and setBool(), to introduce a getString() and setString() for use in
UserSettingsModal while keeping getCached() and setCached() private
within UserSettings.

- Remove unused 'focusLocked' and 'toggleFocusLocked' from UserSettings:
was last changed 11 months ago to just return false. Since then we've
moved to different ways of highlighting and this setting isn't used
anymore. No existing references or callers are left.

- Other files:
-- Have callers call the renamed functions (see point above)
-- Remove userSettings from UILayer and Territorylayer: the variable is
unused in those files. Also remove from GameRenderer when it calls
TerritoryLayer.
-- Cache calls to defaultconfig Theme (which in turn calls dark mode
setting)/Config better in: GameView and Terrainlayer.

### Update on Contents later on
It wasn't really in scope of this PR but further consolidation was
called for. These changes could also pave the way for UserSettingsModal
(main menu) perhaps being partly mergable with SettingsModal (in-game)
one day as it begins to look more like it. Even though UserSettingsModal
still does things its own way, and does console.log where SettingsModal
doesn't, etc. They both have partially different content and settings
but also have a large overlap.

- UserSettings: Removed localStorage call from clearFlag() and setFlag()
which were added after creation of this PR, and were neatly merged in
silence without merge conflicts so i wasn't aware of them yet until now.

- UserSettings: added key constants, exported to use both inside
UserSettings and in files that listen to its events.

- UserSettings 'emitChange': now done from setCached, removed from
setBool, setFlag etc. Also removed from the new setFlag. And from
setPattern even though it emitted "pattern" instead of key name
"territoryPattern"; now it emits the default "territoryPattern" from
PATTERN_KEY which is re-used in Store, TerritoryPatternsModal and
PatternInput.

- UserSettingsModal: made UserSettingsModal call existing toggle
functions in UserSettings, or new or existing getter or setter. We do
not need CustomEvent: checked anymore. In UserSettingsModal, its toggle
functions did not all actually toggle, some like
toggleLeftClickOpensMenu actually just set a value. Based on the
'checked' value of the CustomEvent. But we don't need that 'checked'
value anymore and none of the checks for it inside the toggle functions
in UserSettingsModal, now that we just directly call
toggleLeftClickOpensMenu and others in UserSettings.

- SettingToggle: continuing about not needing CustomEvent anymore: the
old way actually fired two events. The native change event from <input>
and our own CustomEvent from handleChange in SettingToggle. It prevented
handling both events by checking e.detail?.checked === undefined. But
now, the native <input> event is all we need to show the visual toggle
change and trigger @changed in UserSettingsModal which calls the toggle
function.

- Use the toggle functions too from CopyButton and
PerformanceOverlay.ts. In PerformanceOverlay, change in
onUserSettingsChanged was needed because of how setBool works.

- UserSettingsModal 'toggleDarkMode': in UserSettingsModal, removed the
event from toggleDarkMode in UserSettingsModal; nothing is listening to
this event anymore after DarkModeButton.ts was removed some time ago.
Also both UserSettingsModal an UserSettings added/removed "dark" from
the document element. Now that UserSettingsModal calls toggleDarkMode in
UserSettings, we could centralize that. But UserSettings is in core, not
in client like UserSettingsModal. But now that we emit
"user-settings-changed", we could handle it even more centralized and
not have UserSettingsModal or UserSettings touch the element directly.
Instead have Main.ts listen to the event and change it dark mode from
there.

- UserSettings: added claryfing comment to attackRatioIncrement and the
new attackRatio setters/getters, to explain their difference. Noticed a
small omitment in its description and fixed that right away in en.json:
you can change attack ratio increment by shift+mouse wheel scroll or by
hotkey. So made "How much the attack ratio keybinds change per press"
also mention "/scroll."



**BEFORE** (with getDisplayName added back to NameLayer as a fix i will
do soon)
get > getItem in UserSettings
![BEFORE get
getItem](https://github.com/user-attachments/assets/5d2bf8b2-9e68-4c58-9b1f-d5636ee5d7e9)

renderLayer in NameLayer (with getDisplayName added back to NameLayer as
a fix i will do soon)
![BEFORE renderLayer NameLayer
ea](https://github.com/user-attachments/assets/ea12d9a4-2ff3-421b-844c-dbc39e5c3193)

**AFTER** (with getDisplayName added back to NameLayer as a fix i will
do soon)
getCached in UserSettings
![AFTER
getCached](https://github.com/user-attachments/assets/7fb1151f-d289-4420-a257-9fe1f9fbcb8f)

renderLayer in NameLayer (with getDisplayName added back to NameLayer as
a fix i will do soon)
![AFTER renderLayer NameLayer
ea](https://github.com/user-attachments/assets/f844e3d4-d6e5-4774-ba18-ba541f066c76)

## 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-04-06 20:41:57 -07:00
VariableVince b5ca0f9d8f Perf/refactor/fix(NameLayer): about 10% extra improvement (#3540)
## Description:

NameLayer perf part 2 after
https://github.com/openfrontio/OpenFrontIO/pull/3475 with thanks to
@scamiv. Shaves off another 10% or thereabouts, even doing something
extra for a fix (see below).

Also refactor/fixes around NameLayer and PlayerIcons, which is used by
both NameLayer and PlayerInfoOverlay, and underlying function in
GameView.

This would go well with other PR
https://github.com/openfrontio/OpenFrontIO/pull/3481, since this layer
reads multiple settings. Reasoning to not use events and instead rely on
fast caching is explained in that PR.

### Contents

- Fixes:
-- Fixes bug on .dev introduced by wrong assumption by me in previous PR
https://github.com/openfrontio/OpenFrontIO/pull/3475. displayName CAN
change during game, when Hidden Names is toggled from settings, so needs
to be put back in renderPlayerInfo.
-- Fixes longer existing bug: it was assumed Dark Mode didn't change
after creation of icon element. Now it also sets Dark Mode attribute
when updating icons elements.
-- Fixes target mark icons not being shown to team members, while the
icons were shown to normal allies. And EventsDisplay displayed message
"XX requests you attack XX" to both team members and allies already. So
why is the icon not shown to both if the message already is. While we
improve performance of GameView > PlayerView > transitiveTargets (which
is only used by NameLayer/PlayerIcons so only in this context). We can
add team members' targets to it in one go. So previously
transitiveTargets returned: your own targets and allies' targets. Now
transitiveTargets is faster and returns: your own targets and allies'
targets and team members' targets.

- NameLayer:
-- renderLayer: for target icons, getPlayerIcons used to fetch
myPlayer.transitiveTargets each time. While that doesn't change per
player we're rendering for. So now, we fetch myPlayer.transitiveTargets
once per call to renderPlayerInfo, which passes it on to getPlayerIcons.
So now we check it 1x each 100ms (renderCheckRate) inside of
renderLayer. Instead of up to 100s of times each 500ms
(renderRefreshRate) inside of getPlayerIcons inside of renderPlayerInfo
loop.
-- createBasePlayerElement and renderPlayerInfo: use cloneNode where
possible with templates
-- createPlayerElement: only find the elements and set font and flag.
Leave the rest to renderLayer > renderPlayerInfo which fills displayName
and troops and font color very soon after anyway. I haven't noticed a
difference in testing.
-- cache game.config() and others
-- renderPlayerInfo: remove check if render.flagDiv exists, we know it
exists. Check if fontColor changed before assigning it (it never
changes, currently, be it dark mode or light mode). Don't check if
troops or size changes, that happens so often that the overhead for
checking would be smaller than the win, probably.
-- We don't require nameLocation to be changed to change scale (see
previous Namelayer perf PR for the reason). But it seems good to check
if the transform changed before 'overwriting' it, so do that now
instead.
-- Remove Alliance icon DOM traversals. Only do it once, for each time
an alliance icon is displayed. To this end, also made NameLayer more
agnostic on Alliance icon stuff. By moving more code to PlayerIcons. See
below.
-- Use cached allianceDuration instead of fetching this static value
every time
-- Re-use from PlayerIcons: ALLIANCE_ICON_ID, TRAITOR_ICON_ID etc
-- create more sub-functions to make the icons loop in renderLayer more
readable: handleEmojiIcon, handleAllianceIcons,
createOrUpdateIconElement (createIconElement already existed, now
combined), handleTraitorIcon.
For Alliance icons, this was already done in PlayerIcons.ts through
createAllianceProgressIcon (now createAllianceProgressIconRefs), and
more now to skip some DOM traversals. But most of this belongs in
NameLayer itself when it comes to seperation of concern.
- cache dark mode (as boolean and as string)
- use dark mode to update (alliance) icons too, not only on create,
since the setting can change after icon element creation and before it
is removed
-- for getPlayerIcons, add this.alliancesDisabled. If disabled,
getPlayerIcons won't fetch Alliance icon and Alliance Request icon.

- PlayerIcons:
-- use cloneNode where possible
-- added check for alliances disabled: then skip alliance (request) icon
checks
-- See point under NameLayer about the move of Alliance icon code to
PlayerIcons. To make NameLayer even more agnostic on it and keep it in
one place.
-- getPlayerIcons: skip creating a new Set from
myPlayer.transitiveTargets() each time getPlayerIcons is called. One
allocation less. Just do .includes on the returned array. Probably just
as fast in this case, also because not many Targets are present many
times anyway.
-- getPlayerIcons: on outgoingEmojis(), use .find() instead of .filter()
since we only use the first result anyway and it saves us another
allocation.
-- getPlayerIcons: for nukes, only fetch the ones from the player we're
rendering for, not all game nukes. Also don't use .filter() and just a
normal loop to skip an allocation. Logic outcome is the same.
-- getPlayerIcons: for target icons, it used to fetch
myPlayer.transitiveTargets each time. While that doesn't change per
player we're rendering for. So now, NameLayer fetches
myPlayer.transitiveTargets once per call to renderPlayerInfo, which
passes it on to getPlayerIcons.
-- Remove the need for querySelector and getElementsByTagName("img")
alltogether. Since this would be done for every time an alliance was
(re-)created, here in createAllianceProgressIconRefs in PlayerIcons it
makes more sense to not do DOM traversal than in createPlayerElement in
NameLayer where we only do it once per player per game anyway. We assume
updateAllianceProgressIconRefs just knows of all image names in
createAllianceProgressIconRefs. This is a bit less dynamic and
maintainable maybe, but i think worth the win. And the functions are all
one-purpose and not meant to be used dynamically by another caller
anyway.
-- So instead of updateAllianceProgressIconRefs looping through
refs.images, now just update the different images each. See point above.

- PlayerInfoOverlay: also re-use the new exported consts from
PlayerIcons. Since we put those in PlayerIcons anyway, need to be
consistent. Even though PlayerInfoOverlay is outside of the scope of
this PR otherwise.
-- for getPlayerIcons, add this.alliancesDisabled here too. If disabled,
getPlayerIcons won't fetch Alliance icon and Alliance Request icon. We
also send includeAllianceIcon = false, which means Alliance icon will
already be excluded but Alliance Request icon is normally still fetched
and shown.

- GameView > PlayerView: for transitiveTargets (only used in
NameLayer/PlayerIcons so only in this context), improve performance. It
did several allocations. Now it loops directly over the arrays we need.
Also (as mentioned under Fixes) previously transitiveTargets returned:
your own targets and allies' targets. Now transitiveTargets is faster
and returns: your own targets and allies' targets and team members'
targets.


**BEFORE**

![BEFORE](https://github.com/user-attachments/assets/02ff167f-7978-4968-a26e-0c64bf4fb2f3)

**AFTER** (including now getting team members' targets for
myPlayer.transitiveTargets)

![AFTER](https://github.com/user-attachments/assets/1b81f9cc-bb8b-4d6b-97e4-f6db3802e55c)

## 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-04-06 20:36:23 -07:00
RickD004 e7029564a2 adds map of the Great Lakes (#3588)
## Description:

Highly requested map. Map of the 5 Great Lakes (Superior, Michigan,
Huron, Erie and Ontario) with adjacent rivers and lakes. Local cities
and towns as nations / NPCs.


https://github.com/user-attachments/assets/157346fa-e3e6-4d63-9f51-46bae002d529

(Kinda obvious we needed this location at some point)

## 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
2026-04-05 19:40:43 +00:00
Patrick Plays Badly 6467007fd7 Add map dyslexdria (#3568)
## Description:
Re Add map Dyslexdria. I adjusted the rivers to make boat traffic go
through main channels isntead of shortcuts. Flags adjusted. Correct
thumbnail added

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

## Discord username 
Plays Badly
2026-04-03 12:50:52 -07:00
evanpelle 6ca5cb9701 Revert "Add multiple funny maps april fools day (#3538)"
This reverts commit a7d9652886.
2026-04-01 20:03:33 -07:00
evanpelle 8b9f714d6f Revert "Fix WorldRotated enum value to match lang key 🌐 (#3553)"
This reverts commit 4dda807b03.
2026-04-01 20:02:56 -07:00
evanpelle ff7f459825 Revert "Add Map - Dyslexdria - April Fools (#3413)"
This reverts commit 2bf96db464.
2026-04-01 20:02:37 -07:00
evanpelle 85f09c1c98 Revert "Make april fool maps have the same name as normal maps (#3555)"
This reverts commit 38be51a2d6.
2026-04-01 20:02:16 -07:00
RickD004 38be51a2d6 Make april fool maps have the same name as normal maps (#3555)
## Description:

Make the april fool maps have the same name as the normal maps, in order
to make the prank better.

NOTE: In order for this to work, it re-adds the normal maps into
rotation. These normal maps have been given a lower rotation however.

In custom matches the maps will appear with the april fools names.

This change will be reverted after April Fools.

Check this for proof of it working: 

https://canary.discord.com/channels/1359946986937258015/1450487807242932336/1488682300924231882

## 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-03-31 18:43:03 -07:00
Patrick Plays Badly 2bf96db464 Add Map - Dyslexdria - April Fools (#3413)
Re-submission, previous request was not branched properly.


## Description:

Add custom map Dyslexdria. Based off of large_world_map_recolor.tif and
rivers from the XL World map. Map is intended for long term use.
Suggesting that map be used as an April Fools gag by replacing thumbnail
and title with 'World' during first run.

<img width="995" height="721" alt="dyslexdria screenshot"
src="https://github.com/user-attachments/assets/8826839f-b4e0-431d-af9c-0b0e43dc601d"
/>

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


## Following:

- Build log, screen shots, and test videos.
https://discord.com/channels/1284581928254701718/1456890656147374080


## Discord username:

PlaysBadly

---------

Co-authored-by: Duwibi <86431918+Duwibi@users.noreply.github.com>
Co-authored-by: VariableVince <24507472+VariableVince@users.noreply.github.com>
2026-03-31 17:50:23 -07:00
FloPinguin 4dda807b03 Fix WorldRotated enum value to match lang key 🌐 (#3553)
## Description:

Fixes this display bug, says "WORLD ROTATED" now

<img width="235" height="140" alt="image"
src="https://github.com/user-attachments/assets/dc8bb97d-350a-4ced-a63d-f3564479ba41"
/>

## 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-31 17:04:18 -07:00
Patrick Plays Badly a7d9652886 Add multiple funny maps april fools day (#3538)
## Description:
Requesting that these 4 maps be added temporarily for the April Fools
holiday. Included are:
- A wider version of the Amazon River to allow for larger ships.
- An edit of The Box with more varied terrain.
- A modification to The World which optimizes play for mobile users.
- A custom remix to Antarctica which re-adds ice to the map.
All maps are edits of existing work. All maps have been added as their
own individual map files rather than edits to existing files for ease of
management.

<img width="912" height="1067" alt="temp"
src="https://github.com/user-attachments/assets/089ef59f-8bdf-4c86-b479-620b8c220c3a"
/>
https://discord.com/channels/1284581928254701718/1483786939332169830

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

PlaysBadly
2026-03-31 12:37:34 -07:00
RickD004 6fa9bf692b Add Mediterranean map (#3523)
## 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
2026-03-30 13:49:53 -07:00
RickD004 217a2c4548 Adds Milky Way Map (#3519)
## 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
2026-03-26 19:18:30 -07:00
evanpelle 7fdda33fb9 Merge branch 'v30' 2026-03-25 13:34:34 -07:00
FloPinguin cf0cf14a1f Add new public game modifiers 🙂 (#3500)
## Description:

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

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

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

I think we have enough modifiers now :) Good variety

## Please complete the following:

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

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

FloPinguin
2026-03-24 12:57:08 -07:00
Evan 39ad547c04 support for unlockable flags (#3479)
## 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
2026-03-23 17:09:18 -07:00