201 Commits

Author SHA1 Message Date
Ryan 106938c395 Add Ranked 1v1 Leaderboard (#3008)
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #(issue number)

@wraith4081 's pr

updates the stats modal to show both 1v1 and clan stats

- [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
regression is found:

w.o.n

---------

Co-authored-by: Wraith <54374743+wraith4081@users.noreply.github.com>
Co-authored-by: iamlewis <lewismmmm@gmail.com>
2026-02-01 15:02:43 -08:00
FloPinguin 2e609fd858 Move betrayal button, remove betrayal confirmation 🔧 (#3076)
## Description:

- Move betrayal button to the boat-sending-button-location (you can't
send boats to allies) to prevent missclicks
- Remove betrayal confirmation

<img width="260" height="248" alt="image"
src="https://github.com/user-attachments/assets/0a25fc9c-c8a0-4ba9-a8c8-971d6a7a0511"
/>

## 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-01-31 13:53:24 -08:00
FloPinguin 965dd0f482 Confirm alliance break ⚠️ (#3033)
## Description:

People accidentally clicked the betray button because it's at the same
position as the ally button.
So let's add a small confirmation step.


https://github.com/user-attachments/assets/754f2d33-7419-42fc-a732-197c3107236e

## 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-01-28 11:30:09 -08:00
Evan 9aed372425 Added afterEach cleanup to call inputHandler.destroy(), which clears the setInterval before jsdom tears down and removes window. (#3030)
## Description:

Fixes the failing test:coverage ci.

## 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-01-26 13:50:58 -08:00
Arkadiusz Sygulski f6454963b2 Pathfinding refinements (#2959)
## Description:

### Short path for multi-source HPA*

Math was not mathing, increased the bounds to 260x260, it is a bit
slower but should work better. The short path was breaking when player
owned a lot of shores. This is because the bounding box of tiles with
less than 120 distance + 10 padding could be as big as 260x260 and the
optimized array was set to 140x140. I made mistake of calculating it as
`2 * (60 + 10)` instead of `2 * (120 + 10)`.

### LoS path refinement

Previously, we ran 2 passes of LoS smoothing on the path. However, since
we are effectively tracing the same path, the line of sight is
essentially the same. This PR makes second line of sight stop on water
tiles with magnitude `n + 1` compared to first path. Practically, this
means it'll attempt LoS exactly 1 tile after previous corner. See
screenshot.

<img width="1299" height="1151" alt="image"
src="https://github.com/user-attachments/assets/726be236-1ff8-406c-896a-02902a762ab0"
/>

### SendBoatAttackIntentEvent

The flow of sending transport ships is currently strange. This PR makes
the flow more sane.

**Old flow**
```
- Player clicks TARGET tile, it can be deep inland
- Client asks Worker for the best START tile to TARGET tile
- Worker answers `false`, since the tile is inland
- Client sends BoatAttackIntent with START=false and TARGET tiles set
- Worker accepts BoatAttackIntent, computes DESTINATION as closest shore to TARGET
- Worker re-computes best START to DESTINATION
- Worker sends boat from START to DESTINATION
```

**New flow**
```
- Player clicks TARGET tile, it can be deep inland
- Client sends BoatAttackIntent with TARGET
- Worker accepts BoatAttackIntent, computes DESTINATION as closest shore to TARGET
- Worker computes START as the best tile to DESTINATION
- Worker sends boat from START to DESTINATION
```

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

moleole
2026-01-19 19:28:45 -08:00
DevelopingTom f367ea1940 Record human/nation/bot conquests (#2949)
## Description:

Conquests are currently mixing all player types.

This is not ideal as people wonders why a 50 player game can lead to
hundred of kills.
Having separate records can also help with achievements and better
balancing.

This PR splits the conquests record into 3 categories: human, nations
and bots.

It is linked to this infra PR:
https://github.com/openfrontio/infra/pull/246

<img width="895" height="497" alt="image"
src="https://github.com/user-attachments/assets/66e49100-8114-4406-84ab-d9627355956d"
/>

While the recorded data make a distinction between bots/nations, it's
only displayed here as a single "bot" category.

## 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-01-18 20:51:12 -08:00
Arkadiusz Sygulski 2fcca8ee26 Pathfinding - optimize naval invasions (#2932)
# Pathfinding pt. 4

https://pf-pt-4.openfront.dev/

## Description:

Hello again! Pathfinding. It's fast, but inaccurate. This PR makes it
more accurate and actually faster. Sadly it is _faster_ because of a
blunder in previous PR (using BucketQueue where MinHeap would be
better), not because of a new tech. More importantly, it is more
accurate. And that's what people apparently want.

## What changed?

Most of the functional changes relate to `SpatialQuery` module. This is
the thingy that answers "we know the target, which tile of my territory
is the best to launch an invasion". To make it compute a path from South
America to the deep inland China river, it has to work on a coerced map,
one with a very small resolution, so small in fact, that every 4096 map
tiles gets compressed to just one pixel. I hope you see where this is
going.

Previously we selected a random coastal tile within this big pixel
(honestly it wasn't random at all, but could very well be for the
illustrative purposes). Now, we try to be a bit more deliberate. Since
we already know the rough location of the probably best tile, we can
exclude all other tiles from the computation. Imagine a player's
territory spans both Americas on global map - that's a lot of shores.
But since we already know the best tile is somewhere close to Miami, the
problem space was greatly reduced, no need to consider all other shores.
But pathing to the target in China from Miami is still crazy expensive.

This is where second trick comes to play - instead of pathing all the
way to China, we select a _waypoint_ in the rough direction of China,
about 100 to 200 tiles away. This way we fairly cheaply select best tile
to launch an invasion towards this abstract point. And chances are, this
point is far enough, the newly computed path is very close to being
optimal. When you throw a dart from far away, the difference between
scoring 10 and missing is very small. This is why aiming in the general
direction of the board - as opposed to the ceiling - is usually good
enough.

## Okay, but what about the crazy paths when I send invasion to the
opposed bank of a river?!

Well, pathing from America to China is cool, but most players wouldn't
notice the difference on such long paths, what about the short ones? We
now try more accurate pathing first and defer to hierarchy only if it
fails. This produces much better paths for short invasions. While the
fix described above ensures the accuracy is improved also on
medium-to-long routes.

## Playground

Yes.


https://github.com/user-attachments/assets/9cf9586f-c99a-416d-b856-8cf0a21c35ed

## CodeRabbit

Grab a 🥕. Remember `tests/pathfinding/playground` is mostly generated
code and go easy on it. It's enough for it to work and do it's job of
visualizing the paths. No need for throughout review of these files.

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

moleole
2026-01-16 16:27:55 -08:00
DevelopingTom 0466eeac13 Add train gold to game info ranking (#2901)
## Description:

The game info panel was missing the gold generated with trains, which
was recently added into the recorded stats.
This PR adds the gold train ranking, grouped with the naval trade.

Visually the game info panel is not matching the new visual identity,
but this PR only focuses on the missing data.

<img width="898" height="482" alt="image"
src="https://github.com/user-attachments/assets/6366e5d2-23b6-40b0-b4d4-1227b5a2f811"
/>


## 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-01-15 12:24:35 -08:00
Himansu Rawal e1d31ef1ee fix: replace setInterval with recursive setTimeout in Master.ts to pr… (#2869)
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #2868 

## Description:

This PR addresses a critical memory leak in the Master server process
(causing ~30GB RAM usage).

The issue was caused by `setInterval` calling `fetchLobbies()` every
100ms. When `fetchLobbies` took longer than 100ms to complete (due to
network latency or load), requests would pile up indefinitely, creating
a massive queue of pending Promises and open sockets.

I have refactored the polling logic into a generic `startPolling`
utility (in `src/server/PollingLoop.ts`) that uses a recursive
`setTimeout` pattern. This ensures that the next `fetchLobbies` call is
only scheduled *after* the previous one has completed (successfully or
failed), preventing any request pile-up.

## Please complete the following:

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

codimo
2026-01-14 09:50:43 -08:00
Evan 42c944c9cc Create ranked type enum, last person not afk wins in 1v1 (#2892)
## Description:

* Add RankedType enum, for now it's just 1v1
* Add new method to MapPlaylist to create 1v1 game config
* Update WinCheck so the last player is declared a winner on 1v1.

## Please complete the following:

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

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

evan
2026-01-13 19:48:14 -08:00
DevelopingTom c80ccaece9 Record train trading stats (#2891)
## Description:

The current gold stats don’t include gold generated by trains, even
though this is a significant part of the economy for many players.

This PR tracks those stats with two values:
- other players trains visits the player station
- the player trains visits any station

Linked to this infra PR: https://github.com/openfrontio/infra/pull/242

## 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-01-14 01:00:55 +00:00
Arkadiusz Sygulski 85def73bd9 Pathfinding Refinement (#2878)
# Pathfinding pt. 3

## Description:

This PR introduces final change to the pathfinding - path refinement. It
optimizes Line of Sight refinement by searching with for the best tile
with a binary search instead of linearly. And then spends the recovered
budget on better refinement of the first and last 50 tiles of the
journey - the place where user is most likely to look at. Additionally
this PR re-introduces magnitude check and makes the ships prefer sailing
close to the coast, but not too close.

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

## What?

| Before | After |
| :--- | :--- |
| <img width="1097" height="1117" alt="image"
src="https://github.com/user-attachments/assets/4a0b300d-10ef-4151-b6dc-33acfb49f992"
/> | <img width="1093" height="1119" alt="image"
src="https://github.com/user-attachments/assets/cf81c515-c145-40f4-91e5-a4353986907b"
/> |
| <img width="1096" height="1129" alt="image"
src="https://github.com/user-attachments/assets/21b46bce-f961-4259-88f6-fe4a66180270"
/> | <img width="1098" height="1126" alt="image"
src="https://github.com/user-attachments/assets/d92587d1-e6b6-4353-b4a4-1efe71bca43d"
/> |

## Performance

There is actually a severe performance impact of these changes. The path
initial path takes almost 2x as long to generate - this is because pre
processing can only do so much if the initial path is ugly. Luckily in
real gameplay we only need to do this calculation once per edge, so the
actual observed performance impact should be much smaller. Cache FTW.

| | No Cache | Cache |
| :--- | :--- | :--- |
| Before | 277.04ms | 208.58ms |
| After | 498.34ms | 264.27ms |

## DebugSpan

Small utility, it allows any code to be easily instrumented for
performance. The idea is the same as with [OTEL
Spans](https://opentelemetry.io/docs/concepts/signals/traces/). Produce
a span, create sub-spans, measure whatever you need. Works only when
`globalThis.__DEBUG_SPAN_ENABLED__ === true`, otherwise no-op.

Cool stuff, try it out:
```ts
// Convenient wrapper, small performance impact
return DebugSpan.wrap('add', () => a + b)

// Synchronous API, basically free
DebugSpan.start('work')
work()
DebugSpan.end()

// Create sub spans
DebugSpan.wrap('complex', () => {
  const aPlusB = DebugSpan.wrap('add', () => a + b)
  DebugSpan.set('additionResult', () => aPlusB)  // Store data
  return aPlusB * c
})

// Access spans, data and timing
const span = DebugSpan.getLast()
const compelxSpan = DebugSpan.getLast('complex')

console.log(complexSpan.duration, complexSpan.data['additionResult'])
```

These are virtually free and can be enabled on-demand **in production**
and available in the devtools. Under the hood devtools integration is
just a wrapper around [Performance
API](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API).
For clarity data keys not prefixed by `$` are omitted from the
integration. Every key prefixed with `$` must be fully JSON
serializable.

<img width="977" height="799" alt="image"
src="https://github.com/user-attachments/assets/b4d43506-1639-4f78-a611-30e61de12a07"
/>
2026-01-13 12:39:54 -08:00
FloPinguin 35b7213c5c Enhance nuke alliance breaking logic to account for allied structures in blast radius 💣 (#2887)
## Description:

Doesn't need a description :D


https://github.com/user-attachments/assets/8de576fd-050b-4b35-8526-e4c88d1a9f25


https://github.com/user-attachments/assets/c99147a1-efdf-426b-96d1-e996e01f89aa

## 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-01-13 12:30:25 -08:00
FloPinguin 464a4a817a Remove hardcoded numPlayersConfig, calculate it based on the maps land tiles 🔧 (#2874)
## Description:

The calculation is based on: 50 players per 1_000_000 land tiles,
limited at 125 players because of performance
Second number is 75% of that, third one 50%
That way, the player counts are staying mostly the same
Look at the "Dynamic Config" column, these are the new player counts:
(The 125 players limit is missing in that column, only relevant for the
twolakes map)

<img width="930" height="1033" alt="Screenshot_2026-01-12_152758"
src="https://github.com/user-attachments/assets/e1791740-e263-47b3-8b27-4f9aa358d381"
/>
<img width="926" height="324" alt="Screenshot_2026-01-12_152814"
src="https://github.com/user-attachments/assets/78d6789b-374f-4f8b-b50f-f6f08395572b"
/>

This PR also removes `MapDescription` from `Maps.ts` because its unused.
And this PR updates the map-generator `README.md` to reflect the changes

## 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-01-12 21:18:47 -08:00
Arkadiusz Sygulski 0e3ced3bfa Pathfinding Refactor pt. 2 (#2866)
## Playtest

https://pf-pt-2.openfront.dev/

## Pathfinding Refactor pt. 2

<img width="1536" height="1024" alt="image"
src="https://github.com/user-attachments/assets/9477958e-54b7-4c83-b317-ba789e809e9e"
/>


This is a follow-up to a previous PR introducing pathfinding changes.
This time, it introduces a complete refactor of `pathfinding` directory
and breakdown into composable pieces.

### Unified PathFinder interface

`PathFinder<T>` and `SteppingPathFinder<T>` are introduced to unify
**all** pathfinding across the application. First one exposes complete
path, while stepping variant allows the callee to iterate over the path
by calling `.next`. All pathfinders share this one common interface,
which makes them easy to use in any scenario -
`PathFinding.Water(game).search(from, to)`.

`SteppingPathFinder<T>` extends `PathFinder<T>` with an ability to
iterate over the path. It handles caching, storing current index and
invalidation. This allows the units to not care about the inner workings
of the pathfinder and just call `pf.next(current, target)` and receive
instructions on what to do next.

### Common entry point

All pathfinders are now exposed from common `PathFinding` entrypoint:

- `PathFinding.Water`
- `PathFinding.Rail`
- `PathFinding.Stations`
- `PathFinding.Rail`

Additional entry point is introduced for pathfinders which need to work
both in the worker, but also on the frontend, which lacks `Game`
interface. Currently only `UniversalPathFinding.Parabola` is available.

### Spatial Query

New module has been introduced close to `pathfinding` - `SpatialQuery`.
It aims to resolve any questions game may have about finding tiles
meeting criteria. Currently `SpatialQuery.closestShore(player, target)`
and `SpatialQuery.closestShoreByWater(player, target)` are available -
they help answering questions about naval invasion: "What is the best
landing location from user's click?" and "Which our tile should be used
to launch the transport ship?". Under the hood they use very similar
mechanics to pathfinding, so it felt right to put them close by.

### Modular architecture

Pathfinders now support transformers: `MiniMapTransformer`,
`ShoreCoercingTransformer`, `ComponentCheckTransformer`,
`SmoothingTransformer`. Transformers functions like a middleware in the
pathfinding chain. They wrap around the pathfinder and provide
additional functionality. This allows the pathfinder to focus on
actually finding the path instead of doing unrelated things.

Example chain for simple (A*) water pathfinding:
```ts
static WaterSimple(game: Game): SteppingPathFinder<TileRef> {
  const miniMap = game.miniMap();
  const pf = new AStarWater(miniMap);

  return PathFinderBuilder.create(pf)
    .wrap((pf) => new ShoreCoercingTransformer(pf, miniMap))
    .wrap((pf) => new MiniMapTransformer(pf, game.map(), miniMap))
    .buildWithStepper(tileStepperConfig(game));
}
```

The Pathfinder - here `AStarWater` - does not care about the conversion
between minimap and main map tiles. It also does not care if the source
or destination is a land tile. The transformers take care of that. The
pathfinder gets a set of valid coordinates and produces the path -
that's it.

Modular approach makes working on a particular set of utilities much
easier - for example map upscaling is handled consistently across all
pathfinders. Additionally, the pathfinders are not tied to the
particular map resolution used. Pass them a different map and they will
work the same.

### Algorithms

Algorithms used are neatly organized inside
`src/core/pathfinding/algorithms`. They are prefixed with the algorithm
name and suffixed with the use case. File without suffix exposes generic
version ready to traverse any graph with adapters. Specialized versions
either use an adapter or inline logic when performance is critical -
using adapters leads to 20-30% performance loss.

The directory includes `A*` and `BFS` but also other useful utils, such
as `AbstractGraph` used to generate... an abstract graph on top of the
tile map and `ConnectedComponents` helping to identify whether two tiles
are connected by a path without actually computing the path.

### Playground

The playground have been updated with new algorithms, including tweaked
very greedy `A*`.

<img width="2175" height="1424" alt="image"
src="https://github.com/user-attachments/assets/1f833651-0024-4299-bf86-882f5368358c"
/>

### Tests

Yeah, there are some, a little too many if I say so myself. But there
are no useless tests. I had to ensure refactored code works somehow
reliably. This PR comes with trust me bro guarantee, but I would
appreciate someone confirming **naval invasions, nukes (esp. MIRV) and
warships**.

### Discord
`moleole`

GL & HF
2026-01-11 20:11:14 -08:00
Stepan Hembarovskyi 97fffaf49e fix: Correctly handles unbound keybinds (#2854)
Resolves #2652 #2291

## Description:

Resolves a regression after a new design changes where keybinds
explicitly set to "Null" were incorrectly reverted to default values
instead of remaining unbound.

Updates the input handler to preserve "Null" as the designated value for
an unbound key. The UI now reflects this by displaying "None" for
keybinds that are set to "Null", providing clear user feedback.

Before/After:
<img width="378" height="371" alt="image"
src="https://github.com/user-attachments/assets/d1d640f2-93aa-47f1-be8e-7d8c68183aa1"
/> <img width="379" height="370" alt="image"
src="https://github.com/user-attachments/assets/f0512cce-c6e9-4180-9517-7669aed76f6f"
/>

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

webdev.js

Co-authored-by: antigrid <Stepan.Hembarovskyi@Inu.edu.ua>
2026-01-10 10:06:47 -08:00
Ryan 5e6c90d9bb Main Menu UI Overhaul (#2829)
## Description:

Overhauls the Main Menu UI, visit https://menu.openfront.dev to see
everything.

## 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-01-09 20:26:34 -08:00
Arkadiusz Sygulski d8762b1317 Fix transport ship src and dst to always be water (#2832)
## Description:

Issue discovered by @DevelopingTom, posted on Discord. After merging
pathfinding PR, transport ships lost the ability to navigate between
land tiles. For now, the quick fix is to select adjacent water tile and
select it for pathing. Conquer logic still applies to original
destination on the shore.

## Please complete the following:

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

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

moleole
2026-01-09 15:01:44 -08:00
Evan 8ff3f4496c Update & improve 1v1 Ranked Matchmaking (#2831)
references #2001

## Description:

Improve the ranked matchmaking modal. Better messages, and show 1v1 elo


<img width="450" height="210" alt="Screenshot 2026-01-08 at 7 11 20 PM"
src="https://github.com/user-attachments/assets/e4f8323c-5d98-48de-babe-b51526a6d408"
/>

<img width="622" height="614" alt="Screenshot 2026-01-08 at 7 11 14 PM"
src="https://github.com/user-attachments/assets/73d10f84-b5b5-4ba8-95bb-a181a9fd9dae"
/>



## 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-01-08 19:18:04 -08:00
Mattia Migliorini 2dada6f516 Handle Nation win condition (#2824)
Resolves #2823

## Description:

When playing in single-player mode, if an NPC reaches 80% land control
before the player, the game enters a broken state where:

- The game clock stops
- Win checking stops permanently
- Even if the player later conquers 100% of land, victory is never
awarded
- The game becomes "stuck" in a zombie state.

This PR addresses this allowing Nations to be set as winners in single
mode, and in this case showing a "Nation {nation} has won" modal to the
user. This WinModal is the same as the "{player} has won", with the only
change being the title.

Nation wins in FFA, from the human player perspective:

<img width="1457" height="837" alt="image"
src="https://github.com/user-attachments/assets/1ce569bd-6616-4a23-b4a4-afedad2c64f8"
/>

## 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-01-08 13:51:23 -08:00
Arkadiusz Sygulski b090f2f624 HPA* Pathfinding (#2815)
## Pathfinding with HPA*

Hi! The primary objective of this PR is to replace per-tile A* with
hierarchical pathfinding - HPA*. In practice, this means we create an
abstract graph on top of the actual map with far fewer points and use it
to decide on general path structure. Only then we go back to tile-level
and build path between selected waypoints. This speeds up long distance
pathfinding by over 1000x in some cases. To make the review easier, it
comes with a benchmark and visual playground.

## PREPROCESSING

H part of HPA* means "hierarchical" and requires preprocessing.

This PR includes pre-processing as part inside `new Game()` constructor.
It takes about 135ms for `giantworldmap` on my machine, which increases
the effective initialization from ~95ms to ~230ms. This time could be
reduced in different ways, which are **out of scope** for this PR.

After confirming the initialization time is bearable on low-end devices,
I argue merging this PR as-is is acceptable tradeoff. It creates small
lag at the beginning of a round but pays for itself in the first minute
of the match.

## Nerdy details

**Architecture**
- HPA*-style hierarchical pathfinding
- 32×32 sectors on minimap with gateway nodes on borders
- Gateway graph built via BFS during preprocessing
- Water component optimization skips unreachable gateway pairs
- A* on gateway graph → local A* within sectors → Bresenham path
smoothing
- Minimap upscaling identical to currently used in MiniAStar

**Key Optimizations**
- Typed arrays instead of high-level primitives
- Stamp-based visited tracking (no need to recreate buffers, O(1)
clearing)
- Optional - enabled by default - caching of tile paths between gateways
- Line of sight smoothing for the final path

## Review Focus

Play with included tools, benchmark and visualization. Pathfinding
should be safe to merge as a black box - you do not need to understand
the details. Outcomes can be tested empirically in-game. Visualize (and
share!) edge cases with included playground. Confirm the 100x speedup is
real with benchmark.

If you plan to dive into the code, I suggest the following order:
- Pathfinding abstraction in `src/core/pathfinding/`
- Pathfinding tests in `tests/core/pathfinding/`
- NavMesh in `src/core/pathfinding/navmesh/` + integration with
`Game.ts`
- Benchmark in `tests/pathfinding/benchmark/`

Do not look at playground's code, it has been created with a clanker.
The design is 100% mine and I spent way too long polishing it, but I
haven't even once edited the code manually. There is probably no
abstraction whatsoever, just do not look at the code, let it play.

## Core Changes

#### Pathfinding (`src/core/pathfinding/navmesh/`)
- HPA* + refinement -> three phased pathfinding: A* over the graph ->
naive path -> refinement
- comes with A* and BFS optimized for for specific needs

#### Pre-Processing (`src/core/pathfinding/navmesh/`)
- identify water bodies to avoid pathfinding between disconnected nodes
- create high-level graph of gateways on top of tile map

#### Abstraction (`src/core/pathfinding/`)

- common `PathFinder` interface that can return full path and also act
as state machine (`.next()`)
- adapters for both new and legacy algorithm with fallback to legacy if
navigation mesh not available

#### Benchmark (`tests/pathfinding/benchmark/`)

- `npx tsx tests/pathfinding/benchmark/run.ts` - no guesswork, numbers
- `npx tsx tests/pathfinding/benchmark/run.ts --synthetic` - 1000s of
synthetic paths
- `npx tsc tests/pathfinding/benchmark/generate.ts` - generate more as
needed, test new maps
- includes ONE synthetic scenario to avoid PR bloat, generate more
locally / later

#### Playground (`tests/pathfinding/playground/`)

- `npx tsx tests/pathfinding/playground/server.ts` - visualize paths
with both new and legacy algorithm

## Benchmarks

### Compared with legacy in default - hand picked - scenario:
```
Initialization: 95.95ms -> 227.29ms
Pathfinding: 3038.43ms -> 6.45ms
Distance: 26972 -> 26810 tiles
```

### 42,000 synthetic routes across all maps
```
Running 42 synthetic scenarios with hpa.cached adapter...

 synthetic/achiran                   | Init:    93.42ms | Path:    139.07ms | Dist: 1481630 tiles | Routes: 1000/1000
 synthetic/africa                    | Init:    87.14ms | Path:    155.08ms | Dist: 1829414 tiles | Routes: 1000/1000
 synthetic/asia                      | Init:    57.60ms | Path:    112.55ms | Dist: 1204082 tiles | Routes: 1000/1000
 synthetic/australia                 | Init:    78.18ms | Path:     77.12ms | Dist:  978375 tiles | Routes: 1000/1000
 synthetic/baikal                    | Init:    78.26ms | Path:    152.14ms | Dist: 1600016 tiles | Routes: 1000/1000
 synthetic/baikalnukewars            | Init:    81.44ms | Path:    165.90ms | Dist: 1699283 tiles | Routes: 1000/1000
 synthetic/betweentwoseas            | Init:    29.29ms | Path:    114.99ms | Dist: 1338075 tiles | Routes: 1000/1000
 synthetic/blacksea                  | Init:    30.66ms | Path:     93.14ms | Dist:  949217 tiles | Routes: 1000/1000
 synthetic/britannia                 | Init:    74.12ms | Path:     85.62ms | Dist:  866752 tiles | Routes: 1000/1000
 synthetic/deglaciatedantarctica     | Init:   105.49ms | Path:    192.93ms | Dist: 1574684 tiles | Routes: 1000/1000
 synthetic/didier                    | Init:    81.51ms | Path:    153.70ms | Dist: 1734876 tiles | Routes: 1000/1000
 synthetic/eastasia                  | Init:    49.29ms | Path:    128.63ms | Dist: 1410270 tiles | Routes: 1000/1000
 synthetic/europe                    | Init:    92.55ms | Path:    178.35ms | Dist: 1525216 tiles | Routes: 1000/1000
 synthetic/europeclassic             | Init:    33.50ms | Path:    104.40ms | Dist: 1209759 tiles | Routes: 1000/1000
 synthetic/falklandislands           | Init:    63.00ms | Path:    107.41ms | Dist: 1080251 tiles | Routes: 1000/1000
 synthetic/faroeislands              | Init:    71.91ms | Path:     49.52ms | Dist:  604613 tiles | Routes: 1000/1000
 synthetic/fourislands               | Init:    45.75ms | Path:     78.91ms | Dist:  937439 tiles | Routes: 1000/1000
 synthetic/gatewaytotheatlantic      | Init:    81.00ms | Path:    257.06ms | Dist: 2555551 tiles | Routes: 1000/1000
 synthetic/giantworldmap             | Init:   214.25ms | Path:    220.42ms | Dist: 1976693 tiles | Routes: 1000/1000
 synthetic/gulfofstlawrence          | Init:    45.16ms | Path:     96.05ms | Dist: 1014604 tiles | Routes: 1000/1000
 synthetic/halkidiki                 | Init:    74.68ms | Path:    149.39ms | Dist: 1546781 tiles | Routes: 1000/1000
 synthetic/iceland                   | Init:    58.72ms | Path:     78.16ms | Dist: 1001554 tiles | Routes: 1000/1000
 synthetic/italia                    | Init:    29.78ms | Path:    139.93ms | Dist: 1412024 tiles | Routes: 1000/1000
 synthetic/japan                     | Init:   161.07ms | Path:    118.65ms | Dist: 1154393 tiles | Routes: 1000/1000
 synthetic/lemnos                    | Init:    52.59ms | Path:    136.69ms | Dist: 1481101 tiles | Routes: 1000/1000
 synthetic/lisbon                    | Init:    49.27ms | Path:     86.53ms | Dist: 1032011 tiles | Routes: 1000/1000
 synthetic/manicouagan               | Init:    53.74ms | Path:    110.52ms | Dist: 1307630 tiles | Routes: 1000/1000
 synthetic/mars                      | Init:    29.39ms | Path:     80.55ms | Dist: 1091702 tiles | Routes: 1000/1000
 synthetic/mena                      | Init:    26.37ms | Path:    120.09ms | Dist: 1272751 tiles | Routes: 1000/1000
 synthetic/montreal                  | Init:    26.08ms | Path:    106.77ms | Dist: 1187736 tiles | Routes: 1000/1000
 synthetic/newyorkcity               | Init:    56.60ms | Path:    181.19ms | Dist: 1753875 tiles | Routes: 1000/1000
 synthetic/northamerica              | Init:    96.29ms | Path:    123.02ms | Dist: 1217221 tiles | Routes: 1000/1000
 synthetic/oceania                   | Init:    52.81ms | Path:     51.96ms | Dist:  482373 tiles | Routes: 1000/1000
 synthetic/pangaea                   | Init:    21.29ms | Path:     56.58ms | Dist:  716189 tiles | Routes: 1000/1000
 synthetic/pluto                     | Init:    53.89ms | Path:    141.62ms | Dist: 1304362 tiles | Routes: 1000/1000
 synthetic/southamerica              | Init:    85.19ms | Path:    123.03ms | Dist: 1301403 tiles | Routes: 1000/1000
 synthetic/straitofgibraltar         | Init:    76.68ms | Path:    108.30ms | Dist: 1304592 tiles | Routes: 1000/1000
 synthetic/straitofhormuz            | Init:    38.97ms | Path:     67.78ms | Dist:  754920 tiles | Routes: 1000/1000
 synthetic/surrounded                | Init:    95.35ms | Path:     90.18ms | Dist: 1017142 tiles | Routes: 1000/1000
 synthetic/svalmel                   | Init:    60.58ms | Path:    104.75ms | Dist: 1235501 tiles | Routes: 1000/1000
 synthetic/twolakes                  | Init:    62.05ms | Path:     94.54ms | Dist: 1140807 tiles | Routes: 1000/1000
 synthetic/world                     | Init:    41.43ms | Path:     93.42ms | Dist:  873406 tiles | Routes: 1000/1000

Completed 42 scenarios
Total Initialization Time: 2796.32ms
Total Pathfinding Time: 5026.64ms
Total Distance: 53160274 tiles
```

## Playground

**That's the fun part**. Watch NavMesh running circles around legacy
`PathFinder.Mini` in real time. Debug inner workings, test edge cases,
share URLs for debugging.


https://github.com/user-attachments/assets/34e2e3f5-fbc1-4b1f-917d-820766e98d5d

## Discord Tag
`moleole`
2026-01-08 13:34:18 -08:00
FloPinguin ebcb654825 Added a public game modifier system 😮 For more variety (#2801)
## Description:

Added a public game modifier system. It causes that

5% of public games are played on the compact version of the map
10% of public games have "Random Spawn" activated

Percentages can easily get changed via `DefaultConfig`.
We can also easily add more modifiers.
Modifiers can stack, so in rare cases you will play on a compact map
with random spawn 😄
More variety!

### "Compact Map" modifier implementation

- With the "Compact Map" modifier the lobby max player count gets
reduced to 25% and only 25% of the regular bots and only 25% of the
regular nations will spawn (because the map has only 25% of its regular
size)
- In private lobbies and singleplayer the nation reduction happens too
(When "Compact Map" is enabled).

### Restrictions

- Duos/Trios/Quads team modes do not get Random Spawn (defeats the
purpose)
- Maps with smallest player count < 50 do not get Compact Map in team
games (not enough players after the reduction to 25%). I have calculated
all the possible max player counts.

### How it looks like

Random Spawn modifier:

<img width="528" height="183" alt="Screenshot 2026-01-06 194959"
src="https://github.com/user-attachments/assets/2f729da9-80c3-4548-8205-71129da2a76a"
/>

Very rare case: Two modifiers at the same time and only 10 max players
have been chosen from `[GameMapType.FaroeIslands]: [20, 15, 10]`.
Because of the 75% reduction in player count only 3 players are allowed
(3 is the minimum). I think its funny that you can play a 1v1v1 in rare
occasions 😄

<img width="526" height="184" alt="Screenshot 2026-01-06 194938"
src="https://github.com/user-attachments/assets/834326eb-df03-41b7-b1db-1efa3f1013b5"
/>

### Funny side-effect

Team games with random spawn. That will be interesting. No more "Who is
better in donating troops to the frontline". Instead you have to heavily
coordinate with your teammates.

## 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-01-06 19:37:58 -08:00
DevelopingTom af0b8a8d50 Configurable immunity timer (#2763)
## Description:

Resolve discussions about stalled PR
https://github.com/openfrontio/OpenFrontIO/pull/2460

<img width="724" height="348" alt="image"
src="https://github.com/user-attachments/assets/c2c9fa79-cace-431a-9ca4-b3656612fa9d"
/>

Changes:
- Added a `Player::canAttackPlayer(other)` function to determine whether
a player can be attacked.
- This function is now used in most places where a fight can occur:
    - AttackExecution (land attacks)
    - Naval invasion
    - Warship fight
- Nukes can't be thrown during the truce
- Immunity only affect human players. Nations and bot will fight as
usual, and can be fought against.
- The immunity timer uses minutes in the modal window.

UI:

- The immunity phase is displayed with a timer bar at the top. This is
from the original PR, to be discussed if it's not deemed visible enough:

<img width="632" height="215" alt="image"
src="https://github.com/user-attachments/assets/f5ab9aa0-bd4f-4503-b8d6-b40b121fba65"
/>


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

---------

Co-authored-by: newyearnewphil <git@nynp.dev>
2026-01-03 20:04:48 -08:00
Aotumuri ae6293f6da Add language metadata and enhance language validation tests (#2748)
Resolves #2739

## Description:

Introduce language metadata handling and refactor existing language
checks to validate the existence of language JSON and corresponding SVG
files. Add tests to ensure the integrity of the new metadata structure
and its references.

The lang field is intentionally kept in each language file.
This is because the files are frequently regenerated by Crowdin, and the
field also serves as a hint for management and maintenance.

## 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: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-02 18:45:49 -08:00
Arkadiusz Sygulski d7bcbf54f3 Add tests for MIRV execution (#2767)
## Description:

This is a companion PR to
https://github.com/openfrontio/OpenFrontIO/pull/2765. It implements
tests and a performance benchmark for MIRV.

```bash
$ npm test -- tests/core/executions/MIRVExecution.test.ts

 ✓ tests/core/executions/MIRVExecution.test.ts (9 tests) 71ms
   ✓ MIRVExecution (9)
     ✓ MIRV should launch successfully 10ms
     ✓ MIRV should break alliances on launch 5ms
     ✓ MIRV should separate into warheads 20ms
     ✓ MIRV warheads should only target tiles owned by target player 15ms
     ✓ MIRV warheads should be distributed with minimum spacing 12ms
     ✓ MIRV should display warning message on launch 2ms
     ✓ MIRV should not launch if player cannot build it 2ms
     ✓ MIRV should not launch when targeting terra nullius 2ms
     ✓ MIRV should launch when targeting own territory without breaking alliances 2ms
```

```bash
$ npm tsx tests/perf/MIRVPerf.ts

...

=== MIRV Performance Benchmark Results ===
MIRV target selection - sparse territory x 53.53 ops/sec ±0.48% (71 runs sampled)
MIRV target selection - dense territory x 53.39 ops/sec ±0.57% (70 runs sampled)
MIRV target selection - giant world map (350 targets) x 1,129 ops/sec ±0.98% (90 runs sampled)
```

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

moleole
2026-01-02 10:36:02 -08:00
FloPinguin 23e4bf6725 ☢️ Nations send much better nukes now (Part 1) ☢️ (#2756)
This is a very important PR for HumansVsNations (But also for
singleplayer).
Humans will throw lots of nukes onto nations, but nations didn't do
that. Until now :)

## Refactor

- Moved all the nuking logic to the new file `NationNukeBehavior.ts`
- Moved `randTerritoryTileArray()` and `randTerritoryTile()` to the new
file `NationUtils.ts` because we need that method in multiple places now
- Because we already have an `NationUtils.ts` (It contains the method
`createNationsForGame` for HumansVsNations) I renamed the old one to
`NationCreation.ts` to avoid confusion

## Bug fixed

- `allRelationsSorted()` in `PlayerImpl` returned dead players all the
time... Which caused nations to not attack / send nukes in some cases...

## Nuke-sending features / improvements

- On hard and impossible difficulty, nations no longer make sure that
nukes will only hit inside of their targets border. This logic very
often stopped nations from throwing nukes. Now their nukes are allowed
to hit TerraNullius (=> ocean!). And in team games, it's even allowed
that their nukes hit other non-friendly players as well! This is very
important for HumansVsNations.
- The basic check for SAMs now gets skipped if we are on easy difficulty
(easy nations are not smart enough to do that)
- I improved the basic check for SAMs (medium difficulty) a bit (nations
send less nukes into SAMs)
- On hard and impossible difficulty, we now use the new method
`isTrajectoryInterceptableBySam()` to avoid SAMs completely. It's
mirroring `NukeTrajectoryPreviewLayer.ts` logic a bit.
- I added "perceived cost" to simulate nations saving up for a MIRV
(Otherwise most hard/impossible nations will spend all their gold on
nukes). But if we are in a team game (MIRVs are not relevant) or if we
already saved up for a MIRV, the "perceived cost" gets ignored.
- Updated the "most hated player" selection in `findBestNukeTarget()` to
ignore very weak players. We don't need to throw nukes at players which
we can easily steamroll by land.
- Added `findFFACrownTarget()` to nuke the crown (based on difficulty).
- Added `findStrongestTeamTarget()` to nuke the strongest team.
- Updated `randTerritoryTile()` so that it has a higher chance of
returning the tiles of a
"leftover-nuked-to-death-player-with-some-tiles-left": `if
(p.numTilesOwned() <= 100) {return
random.randElement(Array.from(p.tiles()));}`.
- Changed `const range = nukeType === UnitType.HydrogenBomb ? 60 : 15`
to `config().nukeMagnitudes(nukeType).inner`. Should make more sense.
- Adjusted `nukeTileScore()` to search for units in
`this.mg.config().nukeMagnitudes(nukeType).inner` instead of fixed 25
- Adjusted `nukeTileScore()` to account for unit levels (levels got
ignored previously). Also increased score for ports from 10_000 to
15_000.
- I made sure that nations can nuke EVERY SINGLE TILE from an enemy,
even if the enemy has no structures ("Prefer tiles that are closer to a
silo" can no longer make the `nukeTileScore()` drop too much,
`bestValue` in `maybeSendNuke()` starts at -1 now)
- In the entire nuking logic, factories were missing. Now they are
added.

## Media

Nation team vs. nation team: They are nuking the very last pixels of
red, just like humans would do it 😀

<img width="915" height="683" alt="image"
src="https://github.com/user-attachments/assets/109c7921-b959-4aa9-a971-0d7742971686"
/>

Hard difficulty FFA game: Nations throwing much more nukes. And they are
nuking the crown.


https://github.com/user-attachments/assets/a6e43924-a6ca-4b1a-a578-4e4f8252e383

Lots of nukes flying:


https://github.com/user-attachments/assets/8fc4edad-a6e6-4476-8a86-08cdef58169e

## 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: iamlewis <lewismmmm@gmail.com>
2026-01-01 13:29:46 -08:00
Vahant Sharma 96622779d1 Add tooltips to Win/Loss Score columns in clan stats (#2752)
Resolves #2508

## Description:

Adds hover tooltips to the "Win Score" and "Loss Score" column headers
in the clan stats table to help players understand what these weighted
scores represent.

### Changes Made
- Added tooltip to **Win Score** column: "Weighted wins based on clan
participation and match difficulty"
- Added tooltip to **Loss Score** column: "Weighted losses based on clan
participation and match difficulty"
- Uses native HTML `title` attribute (follows existing codebase
patterns)
- Fully i18n-ready via `translateText()` - other languages will be
translated via Crowdin

### Implementation Details
- **Files Modified**: 2 files, 4 lines total
  - `resources/lang/en.json`: Added 2 tooltip translation keys
- `src/client/StatsModal.ts`: Added `title` attributes to table headers
- No breaking changes

### Expected Behavior
When hovering over "Win Score" or "Loss Score" column headers, users see
a tooltip explaining the weighted scoring system based on clan
participation and match difficulty.

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

![correctimplementation](https://github.com/user-attachments/assets/bbbad9ab-36cf-4364-96ff-feff6ee966cd)
2026-01-01 10:26:34 -08:00
Ryan 3dcd38a58d lobby websocket instead of polling (#2727)
## Description:
Changes game lobbies into websockets instead of polling

## 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: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: iamlewis <lewismmmm@gmail.com>
2026-01-01 09:38:33 -08:00
evanpelle b5a0541202 Revert "Fix Ctrl+Click for Macs, and alphabetize gitignore (#2720)"
This reverts commit 40eea22007.

Commit breaks map dragging ability.
2025-12-30 16:50:57 -08:00
Bruce Clounie 40eea22007 Fix Ctrl+Click for Macs, and alphabetize gitignore (#2720)
Resolves #2719 
- [X] Ran `npm run format`
- [X] Ran `npm run lint:fix`
- [X] Ran `npm test`
- [X] Manually tested fixes

## Description:

### What problem(s) was I solving?

On Mac, `Ctrl+Click` is commonly used as a substitute for right-click to
open context menus. However, in OpenFront, `Ctrl+Click` was triggering
both the context menu, AND causing an attack.

This effectively prevented Mac users from being able to ally with
bots/nations, as trying to ally would cause you to attack them.

### What changes did I make?
- `Ctrl+Click` on Mac no longer triggers an attack, allowing players to
properly interact with the alliance system

### How I implemented it

1. Added an `isMac()` method to `InputHandler` which encapsulates the
"is mac" logic, and lets it be mocked during tests
2. In the `onPointerUp` handler, added a check: if on Mac and
`ControlLeft` is held, emit a `ContextMenuEvent`, and then return
(instead of continuing to also create a `MouseUpEvent`)
3. Extracted magic numbers for mouse button checks into descriptive
helper methods (`isMiddleMouseButton`, `isNonLeftMouseButton`) for
improved code clarity
4. Added clarifying comments throughout the pointer event handlers

Last, alphabetized the `.gitignore` file and organized it into "Folders"
and "Files" sections to make it easier to read.

### How to verify it

#### Manual Testing
- [X] On a Mac, hold `Ctrl` and left-click on another nation - verify
the context menu opens (not an attack)
- [X] On a Mac, right-click should still open the context menu as
expected
- [X] On Windows/Linux, `Ctrl+Click` continue to work as before
(modifier key for build menu if configured)
- [X] Regular left-click still triggers attacks/interactions as expected

#### Automated Testing
- [x] New unit test added: `Mac Ctrl+Click Context Menu` - verifies that
`Ctrl+Click` on Mac emits `ContextMenuEvent` instead of `MouseUpEvent`

### Description for the changelog

Fixed `Ctrl+Click` on Mac to properly open the context menu instead of
triggering an attack, restoring the ability for Mac users to form
alliances with other nations.

## 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:
Terekhov
2025-12-31 00:26:00 +00:00
Ryan f1561df470 Bomb Direction (#2435)
Resolves #2434 

## Description:

Allows bomb direction to be inverted by pressing a hotkey - currently
"U".

**Check the issue for screenshots / videos.**

## 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: Evan <evanpelle@gmail.com>
Co-authored-by: iamlewis <lewismmmm@gmail.com>
2025-12-29 09:03:46 -08:00
Wraith 26f5d40819 build: migrate build system to Vite and test runner to Vitest & Remove depracated husky usage (#2703)
- Replace Webpack with Vite for faster client bundling and HMR.
- Migrate tests from Jest to Vitest and update configuration.
- Update Web Worker instantiation to standard ESM syntax.
- Implement Env utility in `src/core` for safe, hybrid environment
variable access (Vite vs Node).
- Refactor configuration loaders to remove direct `process.env`
dependencies in shared code.
- Update TypeScript environment definitions and project scripts for the
new toolchain.
- Remove the [depracated usage of the
husky](https://github.com/typicode/husky/releases/tag/v9.0.1).

## Description:

migrate build system to Vite and test runner to Vitest & Remove
depracated husky usage

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

wraith4081

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
2025-12-28 22:10:26 -08:00
Achim Marius 5a065d71c5 Fix alliance renewal popup not being removed when alliance is broken (#2722)
Resolves #2464

## Description

This PR fixes a bug where the alliance renewal popup remained visible
after an alliance was broken or betrayed.
The issue occurred because renewal UI events were tied to player
identifiers instead of the unique allianceId.
When a player had multiple alliances, breaking one alliance did not
correctly remove the associated renewal popup.
This change ensures that renewal popups are correctly removed only for
the specific alliance that was broken.


### What was wrong
Alliance renewal UI events were previously associated implicitly with
players.
This caused incorrect behavior when a player had **multiple alliances**,
because **players are not unique identifiers of an alliance**.

As a result, breaking one alliance could leave stale renewal popups
visible.

### What was changed
- Alliance break logic now relies on **allianceId**, not player IDs
- Renewal popups are removed **only for the specific broken alliance**
- Alliances involving the same player but different allianceIds are
unaffected
- Added tests to ensure this bug cannot reappear

### Result
- Renewal popup disappears immediately when an alliance is
betrayed/broken
- No unintended removal of other alliance renewal popups
- Correct behavior even when a player is involved in multiple alliances

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

assessin.
2025-12-28 13:35:05 -08:00
FloPinguin 5d52f73278 The clown is gone! 🤡 Nations send much better emojis now (#2696)
## Description:

Previously, nations just spammed these two rather toxic emojis: 🤡😡

They now send fewer emojis while attacking, and the clown emoji is
reserved for special cases.

They got the ability to send emojis in much more cases:
- Human didn't donate enough for relation update
- Human did donate an ok amount
- Human did donate a lot
- Responding to emojis that they get sent from a human
- Nuke sent
- MIRV sent
- Retaliation warship sent
- Traitor tries to ally
- Threat asks for / accepts an alliance request
- Disliked human tries to ally
- Friendly human tries to ally
- They are getting attacked by very much troops
- They are getting attacked by very little troops
- Congratulating the winner
- Bragging with their crown
- Charming their allies
- Clown-Emoting traitors
- Easteregg: Sending a rat emoji to very small humans

## 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: iamlewis <lewismmmm@gmail.com>
2025-12-26 13:07:31 -08:00
Evan 02a6ac58ea Cleanup unused deployment secrets & args (#2698)
## Description:

* Remove unused otel creds
* Remove unused R2 creds
* remove left-over BASIC_AUTH
* Generate an admin token on startup
* Removed kick_player since lobby creators already have ability to kick
player

## 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
2025-12-25 19:38:17 -08:00
evanpelle 3a01e15c49 Merge branch 'v28' 2025-12-25 16:03:17 -08:00
Evan a9012a6613 Move cloudflare tunnel creation back into startup.sh (#2694)
## Description:
Reverts #49b01d8

Move cloudflare tunnel creation back into startup.sh. This keeps the
infra outside of the main app.
## 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:

evan
2025-12-25 13:24:23 -08:00
FloPinguin 86d1ac6c62 Nations now counter warship infestations 🚢 (#2658)
## Description:

Relevant for singleplayer and HumansVsNations: 
Humans sometimes try to flood the entire ocean with warships. The goal
is to dominate the trade and to block transport ships.

The already existing `trackTransportShipsAndRetaliate` and
`trackTradeShipsAndRetaliate` methods can't stop these large scale
infestations, the nations are completely helpless.

The new `counterWarshipInfestation` method checks if a nation is one of
the top 3 richest players (Enough money for warships) and if any enemy
(or enemy team) has accumulated more than 10 (for teams total 15)
warships, then builds a counter-warship targeting that threat.

This feature only activates on Hard or Impossible difficulty.

Thats how it can look, nations send out a warship every couple of
seconds, until the infestation threat is gone:

<img width="779" height="670" alt="Screenshot 2025-12-20 160600"
src="https://github.com/user-attachments/assets/25040077-e7db-4720-aea4-7c230afe05ea"
/>

## 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
2025-12-24 10:07:44 -08:00
VectorSophie 00babf4289 Rework fluentslider component and write tests (#2682)
## Description:

After 2 months of vacancy(my bad sorry), i have returned to end this
mess of a PR stain that i left to the codebase.
The issue is fixed, my written tests are passing, and i hand-tested and
it worked out.
Also I had to transforms Lit's ES modules into commonJS format so jest
can execute for my test,
which will indirectly enable other Lit components to be able for
testing(only my test for now)
refer to #2148 for UI stuff, nothing changed 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:

jackochess
2025-12-24 06:24:37 -08:00
Danny 88c6e40399 Remove RNG from SAM launchers (#2665)
## Description:
SAMs will now always hit their target instead of missing sometimes.

Describe the 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:

Lavodan
2025-12-23 08:08:02 -08:00
Danny 7db8d51bf7 Remove RNG from SAM launchers (#2665)
## Description:
SAMs will now always hit their target instead of missing sometimes.

Describe the 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:

Lavodan
2025-12-21 22:49:10 +00:00
Mykola 6112547273 Improve random spawn (#2503)
## Description:

This is a previously approved PR with an additional commit that fixes
case when nations change spawn & jump around, their previous territory
wasn't getting deleted.

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

nikolaj_mykola

---------

Co-authored-by: Evan <evanpelle@gmail.com>
2025-12-20 13:35:30 -08:00
FloPinguin e554ffb1b0 Cleanup nations (Part 3) 🧹 Remove nation strength (#2649)
## Description:

This PR removes the nation strength. Reasoning:

- It is currently unused. The backstory can be found in #2498
- It forces map-makers to do balancing work, which is probably not a
good idea
- It increases map-making work
- It increases nation balancing complexity by a lot (we need to have all
the json files in mind)
- It makes humans avoid map areas completely because there is always
that one, same, strong nation
- The map lead Nikola123 wants to "not deal with the stupid nations and
their balancing"

If the goal of nation strength was to make them feel different I would
suggest a nation personality system. Nations that love to boat, love to
ally, love to nuke, love to fullsend, etc.

Link to a discord discussion about nation strength:


https://discord.com/channels/1359946986937258015/1360078040222142564/1450973197251117218

## 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
2025-12-19 19:10:01 -08:00
evanpelle e6c9e646d4 fix build rename disableNPCs => disableNations 2025-12-18 20:01:32 -08:00
DevelopingTom f532dab704 Add end of game report window (#2598)
Resolves #1664

## Description:

Add a game ranking window, accessible through the player game history:
<img width="508" height="140" alt="image"
src="https://github.com/user-attachments/assets/51a628d9-628d-44c3-9776-d9b359b94e65"
/>

There is a lot of data players could be ranked with.
Three main ranking categories  with their own sub-categories:

<img width="371" height="264" alt="image"
src="https://github.com/user-attachments/assets/8b3b7c53-c52f-4b96-8039-23180c9181cf"
/>

### Duration:
Rank players according to their survival time

<img width="284" height="281" alt="image"
src="https://github.com/user-attachments/assets/6dfa0d11-7f5b-4f4f-81f8-f31e24ade6bf"
/>


### War:
#### Conquests:
Number of conquered players and bots

#### Bombs:
Show all bomb launched by each players. Can be sorted with each
category.
<img width="289" height="193" alt="image"
src="https://github.com/user-attachments/assets/fc0f9663-9a50-4098-b5c6-f434354accff"
/>

### Economy:
Show all gold earned by each players with trade, conquests, pirate or
total:

<img width="276" height="195" alt="image"
src="https://github.com/user-attachments/assets/a925249d-b2d2-4c61-92a5-4dbf5922b32b"
/>


### Responsiveness:


![ranking_showcase_resize](https://github.com/user-attachments/assets/5316d7f4-803f-4223-b834-783040226b7d)


## 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
2025-12-18 19:41:29 -08:00
FloPinguin 4d5bb7a835 Cleanup nations (Part 1) 🧹 (#2637)
## Description:

1. Using the wording `"Nation"`, `"FakeHuman"` and `"NPC"` at the same
time is confusing.
So I renamed every mention of `"FakeHuman"` and `"NPC"` in the entire
project to `"Nation"`. Just like they are called ingame.

2. `BotBehavior.ts` was originally intended for sharing the logic
between nations and bots.
But at the moment, the logic there isn't really shared and it's
basically just about attacking.
So I renamed `BotBehavior.ts` to `AiAttackBehavior.ts`. I use "Ai" to
indicate that this file is used by bots AND nations.

3. Moved `execuction/utils/AllianceBehavior.ts` to
`execuction/nation/NationAllianceBehavior.ts` to make sure everybody
understands that this file is not about alliances in general. It's just
about nations and how they handle alliances.

4. Removed `difficultyModifier` from `DefaultConfig`. It's unused and I
think we usually want to finetune the difficulty instead of using that
method.

5. Added `assertNever` in all `switch (difficulty)` default cases.

## 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
2025-12-18 16:20:23 -08:00
Danny 58c7cdd46f Task: Unify username validation and remove username sanitation (#2622)
## Description:

This PR centralizes all username validation using UsernameSchema with a
set maximum, minimum, and a regex pattern,

It also removes sanitization, as all places where the username would be
sanitized on the server have been gatekept, so no unvalidated usernames
can get onto the server past the ClientMessageSchema safeParse in
GameServer's on message func.

Here is how the errors look if that happens, Note that if the client is
funtioning correctly and the user doesn't manually send a WS message,
they should never see this. The screenshots are from a debug build where
client uname validation was disabled.
<img height="300" alt="error message too short"
src="https://github.com/user-attachments/assets/1b7ac32c-2f03-40fb-8ce9-1f4ab66100bd"
/>
<img height="300" alt="error message bad regex"
src="https://github.com/user-attachments/assets/c78b4114-7e4b-4d39-a135-4cab3ad52c0b"
/>

Profanity sanitization was not changed.

Additionally, the censor tests were updated to reflect the new
expectations.
Jose was added to the jest config as an allowed transform pattern, as it
didn't make sense to me to mock a zod schema.

The UsernameSchema pattern was set to `^[a-zA-Z0-9_ \[\]üÜ]+$`, I
personally think either we should allow all latin characters (regex has
a pattern for this, `\p{L}` or `\p{sc=Latin}`) and then we'd use some
kind of library to normalize all latin characters into regular ascii for
name filtering, or we should only keep ascii letters.

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

Lavodan


(I just realized sanitization isn't a word, it's supposed to be
sanitation, sorry.)
2025-12-15 10:07:15 -08:00
Evan 71cf309252 increase mirv price with total number of merged launched (#2621)
## Description:

To prevent MAD stalemates, have the price of MIRVs increase after each
launch. This will encourage players to launch a MIRV once they have
enough money for it. Also reduce the price of the first MIRV to 25
million to reduce snowballing, each subsequent MIRV cost an extra 15
million:

1. 25 million
2. 40 million
3. 60 million
4. etc



## 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
2025-12-14 19:52:54 -08:00
VariableVince 4f6a433dc8 Fixes & cleans up Username input (#2619)
## Description:

**Fix:**
Error box like "Username must be at least 3 characters long" sometimes
still remains after the game starts. See bug report with screenshot:
https://discord.com/channels/1284581928254701718/1449169462426079343/1449169462426079343

This is while in main.ts, element username-validation-error is already
set to hidden. Most of the time this works but apparently sometimes
there's a re-render which removes the 'hidden' tag again. Most probable
solution for this edge case: clear validationError first. Then to make
sure, still hide element username-validation-error.

**Cleanup username.ts:**
- Remove cat and clover emojis from validPattern. For single player
games, the only other validity checks are done from GameRunner calling
sanitize() from Util.ts. And from GameImpl where from addPlayer the
PlayerImpl are created, which uses sanitizeUserName also from
username.ts, which uses validPattern again. Both GameRunner and
PlayerImpl therefor allow emoji. But for muliplayer there's an extra
step where ClientJoinMessageSchema enforces UserNameSchema = SafeString.
Which does NOT allow cat or clover emoji. So for multiplayer you get an
error and can't join a game with cat or clover emoji. To align their
behavior more, just disallow the emojis from validPattern from the
start. Almost no-one ever used them anyway, they were once put in
because a developer liked to use them before the existance of
ClientJoinMessageSchema. There's more consolidation/refactoring possible
but this is an important first step.
- Remove sending arg max: MAX_USERNAME_LENGTH for translation key
invalid_chars, since the translation string doesn't contain params

**Cleanup UserNameInput.ts:**
- Remove userSettings: isn't used anywhere in the code
- Remove dispatchUsernameEvent: nowhere in the repo is event
'username-change' listened to. Also, dispatchUsernameEvent is only
called from connectedCallBack but not anywhere else. It seems like
handleChange was made for this. Also main.ts calls
usernameInput.getCurrentUsername() and doesn't listen for the event
either.

## 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
2025-12-14 18:38:25 -08:00
FloPinguin dfe33a05e9 Improved the nation alliance request logic 🤝 Massive upgrade to singleplayer fun (#2606)
## Response to alliance requests

Previously the way nations responded to alliance requests was quite
simple / boring / exploitable. Basically you couldn't ally them if you
had a bad relation with them, or if you had too many alliances.
Otherwise they would just take it.

Now there is a **complete decision tree which is based on the
difficulty**. The nations should also feel more human now.

For example, just like humans, nations will now consider to take an
alliance even if you have a bad relation with them (If you are a
threat).

Also, nations no longer check if YOU have too many alliances. Now they
do what humans do: Check if THEY have too many alliances (they want to
be able to attack somebody).

Another big change is the default case: Previously it was just `return
true`. Now it's `return isAlliancePartnerSimilarlyStrong`. So they do
what humans do: Take a quick look at their troop count before allying
them.

## Sending alliance requests

Previously alliance requests were sent randomly. Quite boring.

Now we use the same decision tree as for responding.

## Alliance extension requests

They also use the same decision tree.

## Tests

Tested it a lot in singleplayer.
I have planned to add unit tests for all the nation/bot stuff in the
upcoming cleanup phase.

## Please complete the following:

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

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

FloPinguin
2025-12-14 15:18:07 -08:00