Commit Graph

328 Commits

Author SHA1 Message Date
scamiv d8841fd1ed Add territory renderer fallback controller 2026-05-26 22:43:23 +02:00
Ryan 7873bdbcb4 clan stats breakdown (#3869)
## Description:

improvements to clan ui. 
<img width="788" height="290" alt="image"
src="https://github.com/user-attachments/assets/736ca147-bff4-44d8-8180-7b80a85556fe"
/>
added "expand all" and new collapsible sections.


<img width="787" height="550" alt="image"
src="https://github.com/user-attachments/assets/deb2f813-854b-46a9-a767-52c4f749f30f"
/>

which changes to collapse all when expanded

also adds more info about team (d,t,q,2,3,4,5,6,7 team)

## 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-05-06 16:10:27 -06:00
Evan df84ee023e Refactor & standardize modal tabs (#3864)
## Description:

Refactors tab handling out of the individual modal components and into
the base o-modal component. Tabs are now declared by passing tabs,
activeTab, and onTabChange props, and a new named header slot pins
consumer-supplied content above the tabs. This standardizes the modal
tab look.

<img width="1089" height="290" alt="Screenshot 2026-05-06 at 12 17
33 PM"
src="https://github.com/user-attachments/assets/08d5a039-0aef-4aa7-b972-1e43b8723685"
/>

## 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-06 12:47:11 -06:00
babyboucher 94bab78d24 Fix off by one error (#3827)
## Description:

Currently it is impossible to search for 2 letter clan tags (UN, FR,
EU), this is because of an off by one error present in the API

## 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-05-05 22:15:47 -06:00
Evan 7f9b63a24c Increase max intent size to 2kb (#3852)
## Description:

Raised MAX_INTENT_SIZE from 500 to 2000 bytes — the move_warship intent
could exceed the old limit and get rejected.
Removed the separate MAX_CONFIG_INTENT_SIZE (also 2000) and the
intentType branching, since both paths now share the same cap.
## Please complete the following:

- [ ] 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-05 14:59:09 -06:00
evanpelle ffbe48ad10 fix: catch punctuation-separated slurs in username censor
Re-add skipNonAlphabeticTransformer to both matcher chains so bypass
attempts like "n.i.g.g.e.r" are detected.
2026-05-04 18:18:36 -06:00
Evan 08b9fd96e6 simplify attack overlay to reduce visual clutter (#3848)
## Description:

Simplifies the attacking-troops overlay: removes the soldier icon and
strength bar, dropping each label down to just the troop number in cyan
(outgoing) or red (incoming) with a soft dark text-shadow halo and no
background fill so territory borders show through cleanly. Also splits
the label into outer (transitioned position) and inner (instant scale)
divs so zoom changes no longer get smeared by the 0.25s cluster-move
transition, retunes the zoom→size curve, and skips incoming labels from
bot tribes to cut clutter.

<img width="374" height="307" alt="Screenshot 2026-05-04 at 5 53 17 PM"
src="https://github.com/user-attachments/assets/a7044221-06cc-4027-b19a-6ff4ca8f542a"
/>

## 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-04 18:10:06 -06:00
Evan 94205426e7 Move turnstile check to api (#3845)
## Description

Re-enables Turnstile verification (was temporarily disabled in v31 to
diagnose intermittent `invalid-input-response` rejections) and moves the
verification call off the game servers.

Game servers no longer hold `TURNSTILE_SECRET_KEY` or hit
`challenges.cloudflare.com` directly. Instead they POST to
`${jwtIssuer}/turnstile` on the api-worker (authenticated with the
existing `apiKey`), which holds the secret and proxies to Cloudflare.
Shrinks blast radius and removes the secret from every game host + GH
Actions workflow.

Response from the api-worker is Zod-validated; null tokens short-circuit
to `rejected` locally.

## Please complete the following:

- [x] I have added screenshots for all UI updates (n/a — server only)
- [x] I process any text displayed to the user through translateText()
(n/a — no user-visible text)
- [ ] 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:

evanpelle
2026-05-04 12:53:02 -06:00
FloPinguin 4cd6f50c03 Nations: Fix city farming + reactive defense posts + fix nuked territory capture 🛡️ (#3814)
## Description:

Would be very good to get these fixes last minute into v31.

- **Farming nations for cities is fixed**: Here you can see city farming
in action, vari farms Finland:
https://www.youtube.com/watch?v=J4J0ajlnSHs&t=137s - Lots of 125k gold
cities... Now nations will build defense posts instead of cities:

- **Nations now build defense posts reactively** instead of through the
normal structure build order. When a nation is under significant land
attack (incoming/own troops >= 35%), it places a defense post near the
attack front -- skipped on Easy, capped at 1 on Medium, and scaled by
the incoming troop ratio on Hard/Impossible. Posts spread along the
front. The old `defensePostValue()` placement path is removed.

- **Nations now capture nuked territory.**
`hasLandBorderWithTerraNullius()` was incorrectly filtering out tiles
with fallout, causing the `nuked` attack strategy to never dispatch a
attack. Big bug

.

- [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-05-03 09:33:44 -06:00
evanpelle f850bdb605 fix(tests): update AttackStats to expect 50% war gold for human conquests 2026-05-02 09:33:14 -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
Evan bcdc2126c6 bugfix: SAM was only reloading when not in cooldown (#3817)
## Description:

reloadMissile() was inside the isInCooldown() block. For level-2+ SAMs,
isInCooldown() returns queue.length === level, so after firing one of
two missiles (queue.length = 1 < level = 2) the SAM is not in cooldown —
meaning expired timers were never cleaned up. Stale queue entries caused
subsequent shots to be treated as still-cooling even after the cooldown
elapsed.

SAM execution now mirrors the missile silo execution

## 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:05:10 -06:00
FloPinguin 0cb17f8086 Fix build: add missing adfree field to ResolveCosmetics test mock 🔧 (#3816)
## Description:

Commit `4d5b7c0` added `adfree: boolean` as a required field to
`UserMeResponseSchema` in `ApiSchemas.ts`, but did not update the mock
object in `tests/ResolveCosmetics.test.ts`. This caused `tsc --noEmit`
to fail with a type overlap error, breaking the production build.

**Fix:** Add `adfree: false` to the `player` mock inside `makeUserMe()`
in the test file.

## 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-05-02 08:32:10 -06:00
Ryan df05d21fc2 Clan System Part 2 - UI (#3625)
## Description:

Continuation from #3276 

Adds the complete client-side clan UI as a Lit web component
(`<clan-modal>`), a typed API client with Zod-validated responses,
shared response schemas, and a reusable `<confirm-dialog>` component.


### New: `ClanModal.ts`

| View | What it does |
|------|-------------|
| **My Clans** | Lists joined clans + pending join requests (built from
`/users/@me`, no extra fetches) |
| **Browse** | Search by tag (min 3 chars), paginated results,
configurable per-page (10/25/50) |
| **Clan Detail** | Stats, paginated + searchable member list, role
badges, join/leave/request actions |
| **Manage** | Edit name (max 35 chars) + description, toggle
open/invite-only, disband |
| **Transfer** | Leadership transfer with member selector + confirmation
|
| **Requests** | Approve/deny join requests (leader/officer) |
| **Bans** | View and unban (leader/officer) |
| **My Requests** | View and withdraw outgoing requests |

### New: `ConfirmDialog.ts`

Reusable `<confirm-dialog>` Lit component — replaces native
`confirm()`/`prompt()` which are blocked or broken on mobile and
CrazyGames iframes. Supports danger/warning variants and an optional
textarea (used for ban reasons). Fires `confirm`/`cancel` events.

### New: `ClanApi.ts`

Typed API client covering all clan endpoints. Every response is
Zod-validated. Auth header is always last in the spread (can't be
overridden by callers). Unknown server error messages always fall back
to a generic client-side string — never displayed verbatim.

### New: `ClanApiSchemas.ts` (in `src/core/`)

Shared Zod schemas for clan API responses with max-length constraints on
`name` (35) and `description` (200). Lives in `core/` so it can be
consumed by both client code and the leaderboard table.

### Modified: `ApiSchemas.ts`

- Added `clans` and `clanRequests` arrays to `UserMeResponseSchema`
- Moved clan leaderboard schemas out to `ClanApiSchemas.ts`
- Renamed `LeaderboardClanTagSchema` → `RequiredClanTagSchema`

### Modified: `Api.ts`

- Added `invalidateUserMe()` to bust the cached `/users/me` response
after mutations
- Removed `fetchClanLeaderboard` (moved to `ClanApi.ts`)

### Tests

- `ClanModal.test.ts` — rendering, view navigation, user actions
- `ClanApiQueries.test.ts` — fetch functions, error handling, pagination
- `ClanApiMutations.test.ts` — join, leave, kick, ban, promote,
transfer, etc.
- `ClanApiBans.test.ts` — ban/unban calls and error paths
- `ClanApiSchemas.test.ts` — Zod schema validation edge cases
- `LeaderboardModal.test.ts` — updated imports

## Notable design decisions

- **Not-logged-in state** — shows "Sign in to join clans" instead of
false "no clans" empty state
- **Rate limit feedback** — reads `Retry-After` header and surfaces wait
time to the user

## 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: evanpelle <evanpelle@gmail.com>
2026-04-30 21:27:35 -06:00
Evan ccb80f4245 Fix warship diagonal chase and improve trade ship capture reliability (#3807)
## Description:

The warship pathfinder operates on a 2x downscaled mini-map, and
upscaling mini-map paths back to full coordinates produces diagonal
interpolated steps. At close range (< 20 tiles), the entire path
consists of these diagonal moves, causing the warship to approach the
trade ship at an awkward angle and never converge cleanly.
## 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-04-30 16:37:44 -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
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
Evan 0c0f9c2a81 Update attack labels (#3784)
## Description:

The motivation behind this PR is to standardize colors & icons for
incoming and outgoing attacks. Outgoing attacks are always aquarious and
incoming are red. This also makes it much easier to see which attacks
are incoming vs outgoing at a glance, as previously the color changed
depending on attack effictiveness. Instead, show a small bar on the left
side that displays attack effectiveness.

<img width="498" height="456" alt="Screenshot 2026-04-27 at 12 58 53 PM"
src="https://github.com/user-attachments/assets/ea6928b3-5dfa-47fa-84d2-63e1e81ef6a4"
/>


Updates the in-game attack labels to match AttacksDisplay: a single
soldier icon recolored via CSS filters, aquarius for outgoing and
red-400 for incoming. Color is now purely directional — the previous
attacker-vs-defender comparison (and the troopAttackColor /
troopDefenceColor helpers that drove it) is gone, along with the
defenderTroops plumbing.

Also adds zoom-aware sizing via a new computeLabelScale(zoom) (full
screen size when zoomed in, linear shrink with a floor so labels never
disappear), bumps font/padding/snap-jump threshold for readability, and
moves immutable per-label DOM writes (icon src/filter, color) into
element creation so the per-tick path only updates the troop count.

Also fixes a bug where the labels kept swapping when 2 clusters where
similar size

## 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-04-27 20:53:09 -06:00
Evan 4aa726cfd8 Serve hashed assets from R2 via CDN_BASE (#3773)
## Description:

Add an optional CDN_BASE env var that prefixes hashed asset URLs from
asset-manifest.json, so the app can serve static assets from R2/CDN
instead of the app origin. The value is determined at runtime via the
EJS template (window.CDN_BASE) — empty string means "same origin,"
matching today's behavior.

A hack to load the worker bundle:

A same-origin Blob script that dynamic-import()s the cross-origin worker
module and buffers early postMessage calls until the imported module's
handler attaches, sidestepping the browser's refusal to construct a
Worker directly from a cross-origin URL.

## 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-04-27 11:27:54 -06:00
FloPinguin 8099b9fad7 Massive nation improvement 🤖 (#3761)
## Description:

- Hard / Impossible nations in team games auto-stop trading with all
enemies
- If there are a LOT of nations on the map (Enzo stream with 400 nation
HvN private games) they no longer start with a city, they start with eco
(port / factory) because they cannot gain much gold from bot-killing
- Impossible nations built way too many missile silos sometimes, caused
by the SAM overwhelming logic. Fixed now.
- In public HvN games with 5M starting gold, nations placed their
structures way too fast, which slowed down their expansion. And humans
could easily cause a lot of damage with one atom bomb. Now their first
structure is a SAM (on hard / impossible) and they wait between their
earlygame structure placements.
- Nations now spread out their port placements more evenly
- Nations are now able to attack much stronger enemies in team games
(They can expect donations)
- Improve performance a bit by adding more early-returns (Dont run any
nuking logic if nukes are disabled, no alliance logic if alliances are
disabled, no boating logic if transport boats are disabled, ...)
- Fix some of the "cannot send troops" messages in the console
(DonateTroopExecution)
- Nations build their first missile silo sooner, they should also build
more SAMs
- Nations spend their gold better after reaching the save-up-target
(previously they stopped nuking)
- Optimized save-up-targets for team games
- The richest impossible nation is nuking very dense players now (lot of
structure levels on a small island)

### How does a 5M gold HvN start look like now?


https://github.com/user-attachments/assets/e9da89c3-c0d4-4144-a741-3101746b16da

## 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-26 18:43:45 -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
Alex Pickett 4338d70420 flag-tweak (#3775)
## Description:
Small server-side data handling adjustment with focused archive
coverage.

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

Please put your Discord username so you can be contacted if a bug or
regression is found:
apick
2026-04-26 21:30:11 +00:00
FloPinguin 4ae1fa2ebe Remove small islands on compact maps with half threshold 🏝️ (#3762)
## Description:

Small-island removal previously ran only on the full-size terrain. The
compact (4x downscaled) map inherited that filtering before being
downscaled, which meant islands that survived at full size could end up
as tiny specks in the compact map.

This PR runs `removeSmallIslands` on the 4x terrain as well, with half
the threshold (15 vs 30 tiles), so very small islands are pruned in
compact maps while the full-size map is unchanged.

## 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-25 11:51:02 -06:00
Evan 54604c4313 Remove directory-URL branch from buildAssetUrl (#3756)
## Description:

buildAssetUrl had a middle branch that, when called with a directory-ish
path (e.g. assetUrl("maps")), would check whether any manifest entry had
that prefix and return an unhashed /_assets/<path> URL. This was wrong:

* The /_assets/ prefix implies a managed, hashed asset — but the
returned URL is neither. It won't exist on R2 after the migration.
* It defeats cache-busting, since no hash is applied.
* It encourages callers to reconstruct asset paths by hand via string
concatenation, bypassing the manifest as the single source of truth.
Renaming or reorganizing an asset directory would silently break callers
the manifest can't help locate.

## 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-04-25 09:33:46 -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
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
Ryan c3d7d0373e Improve ingame moderation for admins (#3678)
## Description:

Players with the `admin` flare can now kick players from any game
(including public lobbies), not just the lobby creator in private
lobbies.

## 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-04-20 11:09:04 -07:00
RickD004 52033597ef Update Britannia and take its Classic version off rotation (#3710)
## Description:

Similar to how Europe classic was taken off rotation, we do the same
with Britannia. The newer Britannia was slightly updated to include the
rivers of the classic version, this is because of feedback from players
in the discord server who say the rivers in the classic version are an
important part of gameplay. Map otherwise keeps same nations and size.

## 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-18 21:36:49 -07:00
Ivan Batsulin 20f428d19e Fix middle-click SAM not upgrading other buildings when SAM is unaffo… (#3670)
Resolves #3511

## Description:
When middle-clicking a SAM launcher you own, the game calls
findAndUpgradeNearestBuilding which queries all upgradeable structures
near the clicked tile. If you can't afford the SAM upgrade, canUpgrade
is false for the SAM (because canBuildUnitType returns false when gold <
cost), so the SAM is excluded from the candidates list. If a nearby
building (e.g. a Factory) can be upgraded, it gets picked as the
"nearest" target and is upgraded instead — spending gold the player was
saving for the SAM.

The fix: after finding the best upgrade candidate, check if there's an
owned SAM within the same search radius as the clicked tile. If there
is, but the best candidate is not a SAM (meaning the SAM couldn't be
afforded), do nothing instead of upgrading the other building. This
ensures middle-clicking a SAM either upgrades it or takes no action.

##Please complete the following:

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

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

fghjk_60845
2026-04-18 21:35:23 -07:00
FloPinguin 12b06fa0b2 Pathfinding Fixes (Water Nukes / Lakes) 💧 (#3714)
## Description:

Fixes water-pathfinding errors that started appearing after the first
water nuke and persisted across the rest of the match.
Users reported warships "getting stuck" (stopped moving).

<img width="374" height="281" alt="image"
src="https://github.com/user-attachments/assets/de38b8f1-c4d8-469e-b3a7-d0cef4dfb772"
/>

### Summary

- The new `AbstractGraphBuilder.buildClusterConnectionsFromCache` was
buggy _(The cached edge costs reused by "clean" clusters were keyed by
tile pair without their original `(clusterX, clusterY)`, so a boundary
edge could be re-stamped with the wrong cluster and become untraversable
by the query-time single-cluster bounded A*. The cache now stores `{
cost, clusterX, clusterY }` and `buildClusterConnectionsFromCache`
preserves the original attribution when re-adding the edge.)_
- Warships: `findTargetUnit` now skips trade ships that are not in the
warship's water component, avoiding pathfinding to provably unreachable
targets.
- Warships: On `patrol` `NOT_FOUND`, clear `targetTile` so the warship
picks a new target. This is a defensive guard for the rare case where a
water nuke splits the component between target selection and pathfinding
- without it, the warship retries the same now-unreachable target every
tick and spams the log forever.

### Test

- Added a Warship test verifying that trade ships in a different water
component are not targeted.

## 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-18 21:32:21 -07:00
Ivan Batsulin e5e1211480 feat: add Shift+ modifier support for keybinds (#3679)
## Description:

This PR adds support for `Shift+<key>` keybind combinations across the
entire keybind system.

Previously, keybinds only supported a single key (e.g. `KeyB` for boat
attack). Now any keybind can be configured as `Shift+KeyB`, which will
only trigger when Shift is held down simultaneously.

Enables to use Shift + A for "select all" feature from #3677 

**Changes:**
- `InputHandler.ts`: Added `parseKeybind()` helper that parses
`"Shift+KeyB"` → `{ shift: true, code: "KeyB" }`. Added
`keybindMatchesEvent()` for consistent matching across all keyup/keydown
handlers. Updated `resolveBuildKeybind()` and all keybind comparisons to
respect the shift modifier.
- `SettingKeybind.ts`: When recording a keybind, lone modifier keys
(Shift, Ctrl, etc.) are skipped — the component waits for the actual
key. If Shift is held when the key is pressed, the value is stored as
`"Shift+<code>"`.
- `Utils.ts`: `formatKeyForDisplay()` now handles the `Shift+` prefix,
displaying e.g. `"Shift+B"`.
- `tests/InputHandler.test.ts`: Added 6 tests covering Shift+ keybind
matching, negative cases (plain key not triggering Shift-bound action),
coexistence of `Digit1` and `Shift+Digit1` on different actions, and
Numpad alias support with Shift.

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

<img width="2255" height="2070" alt="CleanShot 2026-04-15 at 20 23
25@2x"
src="https://github.com/user-attachments/assets/96c19fc3-6294-40b7-82eb-3fde52b71618"
/>


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

fghjk_60845
2026-04-16 19:46:01 -07:00
evanpelle 565060f346 Merge branch 'v30' 2026-04-16 19:40:05 -07:00
Ivan Batsulin 76f8441b45 feat: add warning news type and Firefox performance notice (#3680)
## Description:
Adds a new `warning` news type to the news banner system and uses it to
display a Firefox performance notice.

Changes:
- Added `warning` type with red styling to `NewsBox.ts`
- Added `news_box.warning` key (`"WARNING"`) to `en.json`
- Added Firefox performance notice to `resources/news.json` using the
new `warning` type
- Added `news_box.*` dynamic key pattern to `TranslationSystem.test.ts`
to fix unused key detection

## 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 change:
<img width="2101" height="1770" alt="CleanShot 2026-04-16 at 15 04
35@2x"
src="https://github.com/user-attachments/assets/7a8b9290-4216-4799-b271-606afd9b8723"
/>



## Please put your Discord username so you can be contacted if a bug or
regression is found:
fghjk_60845
2026-04-16 16:53:38 -07:00
Ryan 1509bc9f13 Account Modal Bugfix (#3687)
## Description:

Fix null stat values from LEFT JOIN causing Zod validation failure on
player profiles

https://github.com/openfrontio/infra/pull/316 switched playerStats from
innerJoin to leftJoin so that sessions with no stats row (games that
ended instantly on spawn) are still counted in wins/losses/total.


## 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-04-16 13:18:55 -07:00
evanpelle b32b81c86c Censor slurs split across clan tag and username
Catches names like "[HIT]LER" where neither the clan
tag nor the username is profane on its own, but concatenating them
forms a slur. Detected by running the matcher against clan+name and
checking whether any match spans the clan/name boundary.
2026-04-16 12:36:48 -07:00
Evan 1ebac8e854 Move brand images to proprietary/ and support multi-dir asset pipeline (#3662)
## Description:

* Move proprietary brand images (logos, favicon) from resources/images/
to proprietary/images/ to separate open-source assets from proprietary
ones
* Extend the asset pipeline (PublicAssetManifest, vite.config.ts) to
support multiple source directories (resources/ + proprietary/), so
buildAssetUrl resolves assets from either location transparently
* In dev, serve proprietary/ as a fallback middleware (registered after
Vite's publicDir handler) so resources/ takes precedence when files
exist in both. The idea is we could have placeholder assets placeholders
that can be used by forks, and only the production build uses
proprietary assets.

## 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-04-16 08:42:11 -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
Ryan f32994fbc7 Account Modal Bugfix (#3687)
## Description:

Fix null stat values from LEFT JOIN causing Zod validation failure on
player profiles

https://github.com/openfrontio/infra/pull/316 switched playerStats from
innerJoin to leftJoin so that sessions with no stats row (games that
ended instantly on spawn) are still counted in wins/losses/total.


## 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-04-15 15:14:11 -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
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
Evan 616ba1c794 Add support to purchase cosmetics with in-game currency (#3648)
## Description:

Caps & Plutonium can be used to purchase different cosmetics. 

* The cosmetic button can display pluto/caps/dollars
* Create a "purchaseCosmetic" helper function that handles purchase
logic

## 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-04-13 10:19:43 -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
RickD004 95d7895740 Update Europe map and remove its Classic version off rotation (#3647)
## Description:

This was a proposal in the map channel of the dev discord server:

**Updates the Europe map to include Iceland, and removes Classic Europe
off rotation.** Classic Europe will remain in custom private map list

The only thing the new europe map didnt have from the classic version
was iceland, so i figured we should update the europe map to contain it,
since Iceland is a popular spawn in the classic version. Iceland is in
the same position as the classic map

The classic europe is frankly a lesser version of the new map as it
doesnt contain rivers , is smaller and the terrain has less quality, and
with the updated version, classic would just take up very needed space
in the lobby queue. We currently have a very large number of maps, which
results in players having to wait for a long time for an specific map in
public lobbies. This should help the issue a little at the very least.

<img width="2905" height="1674" alt="image"
src="https://github.com/user-attachments/assets/da98d935-b927-4e04-9383-9a1f2b794f97"
/>

## 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-12 14:04:30 -07:00
Evan d5a2cc0fca cosmetic refactor (#3628)
## Description:

The motivation is to have a single "cosmetic-button" element, so we can
abstract out the cosmetic types. This will make it much easier to add
new cosmetic types in the future.

Unifies PatternButton and FlagButton into a single CosmeticButton
component. Extracts a resolveCosmetics() function that flattens patterns
× color palettes + flags into a ResolvedCosmetic[] with relationship
status pre-computed, replacing duplicated resolution logic across four
callers.

* New CosmeticButton — renders patterns or flags based on
ResolvedCosmetic.type
* New resolveCosmetics() — centralizes ownership/purchase/blocked
resolution
* Extracted PatternPreview — canvas rendering split into its own module
* Added type: "pattern" | "flag" discriminator to Zod cosmetic schemas
* Deleted FlagButton.ts and PatternButton.ts
* Added 320-line test suite for resolveCosmetics


## 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-04-09 21:07:07 -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
VariableVince 646d7ecaf6 Disable Radial Menu during spawn phase, just left click or tap to pick spawn point (#3611)
## Description:

During spawn phase, disable Radial Menu further. Its options where
already greyed out or non-responding on purpose, except for the attack
button (middle button) which could only be used to select a spawn point
but two clicks for that is convoluted.

It was mostly a nuisance, especially on mobile where you where forced to
go through the Radial Menu, so tap and then tap again to pick a spawn
point.

- Now, left click directly places a spawn point. Even if "Left click
opens menu" is enabled.
- And right click does not open Radial menu anymore. Which had no use
anyway. And also makes touch screen and mouse players more alike in that
they now both have no access to the Radial Menu (which didn't have a
purpose anyway except picking spawn point in a convoluted way with two
clicks).
- On touch screen during spawn phase, the Radial Menu also doesn't open
anymore. Instead of two taps (open Radial Menu > tap attack button), now
one tap suffices to pick a spawn point just like one left mouse click
now does.

Fixes https://github.com/openfrontio/OpenFrontIO/issues/3609

Also from UnitLayer > onTouch:
- remove redundant isValidRef check. Since isValidCoord check was added
in PR 3226 above it, we know it is a correct coord and with that correct
ref, already.
- remove redundant comment about isValidCoord/Ref not being checked
there yet intentionally, because it is actually being checked there
since PR 3226.

## Please complete the following:

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

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

tryout33

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
2026-04-08 14:49:51 -07:00
Alex Besios 55e8a4edb7 feat: add NewsBox component and integrate news items into PlayPage (#3545)
Resolves #2998 

## Description:

Adds a news box to the lobby homepage that advertises upcoming clan
tournaments, weekly tournaments, and new player tutorials. The component
sits above the username input and cycles through items automatically.

<img width="1138" height="591" alt="screenshot-2026-03-31_00-48-33"
src="https://github.com/user-attachments/assets/4b79287d-6aca-4c81-9bfe-36aad043f381"
/>

<img width="1107" height="595" alt="screenshot-2026-03-31_00-48-24"
src="https://github.com/user-attachments/assets/598e6b8b-e0f2-4864-a5fb-a00c0cc98f37"
/>

<img width="1367" height="599" alt="screenshot-2026-03-31_00-48-04"
src="https://github.com/user-attachments/assets/14f74e70-9dc0-4d67-af6e-c4708e539490"
/>


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

deathllotus

---------

Co-authored-by: Evan <evanpelle@gmail.com>
2026-04-08 10:53:02 -07:00
evanpelle ea7af50b25 fix Privilege check after bad merge 2026-04-06 21:28:47 -07:00
Cameron Clark 18da7134c8 Implement FX sound effects (#3394)
## Description:
Adds sound effects for approved events from the [sound asset
pack](https://drive.google.com/drive/folders/1KpGYJkmLxipy8XmTeyHf40XDC4P--Ck8?usp=sharing).
15 new sound effects triggered from `FxLayer`, `EventsDisplay`, and
`RadialMenu`. Sounds play even when visual FX are off, so disabling
explosions doesn't kill audio. Unapproved sounds are included as assets
but not wired up yet.

### SoundManager architecture

Reworked `SoundManager` per [maintainer
feedback](https://github.com/openfrontio/OpenFrontIO/issues/1893#issuecomment-4184649434)
and [follow-up
review](https://github.com/openfrontio/OpenFrontIO/pull/3394):

- No more singleton. `SoundManager` is instantiated in
`createClientGame()` with `EventBus` and `UserSettings`
- Layers emit events (`PlaySoundEffectEvent`,
`SetBackgroundMusicVolumeEvent`, `SetSoundEffectsVolumeEvent`) via
EventBus instead of holding a `SoundManager` reference
- `SoundManager` subscribes to these events in its constructor
- `SoundEffect` is a type union (not an enum), per project convention
- All sound configuration (type, URL mapping, events) lives in
`Sounds.ts`
- Sound effects are lazy-loaded on first play
- Channel limit of 8 concurrent sounds. New sounds always play; when at
the limit, the oldest active sound gets stopped
- `SoundManager` bootstraps volume from `UserSettings` in its
constructor
- All Howler calls are wrapped in try/catch with error logging, so sound
failures never crash the game
- `dispose()` method unsubscribes from EventBus and unloads all Howl
instances on game shutdown
- Sound code stays entirely in `src/client/`, nothing in `core/` touches
it

## Sound approval status (per
[spreadsheet](https://drive.google.com/drive/folders/1KpGYJkmLxipy8XmTeyHf40XDC4P--Ck8?usp=sharing))

### Approved, wired up in this PR

| Event | Sound file | Trigger location |
|-------|-----------|-----------------|
| Message sent/received | `message.mp3` | EventsDisplay |
| Menu open/select | `click.mp3` | RadialMenu |
| Atom bomb launch | `atom-launch.mp3` | FxLayer (unit created) |
| Atom bomb / MIRV hit | `atom-hit.mp3` | FxLayer (reached target) |
| Hydrogen launch | `hydrogen-launch.mp3` | FxLayer (unit created) |
| Hydrogen hit | `hydrogen-hit.mp3` | FxLayer (reached target) |
| MIRV launch | `mirv-launch.mp3` | FxLayer (unit created) |
| Alliance suggested | `alliance-suggested.mp3` | EventsDisplay |
| Alliance broken | `alliance-broken.mp3` | EventsDisplay |
| Port built | `build-port.mp3` | FxLayer (construction complete) |
| City built | `build-city.mp3` | FxLayer (construction complete) |
| Defense post built | `build-defense-post.mp3` | FxLayer (construction
complete) |
| Warship built | `build-warship.mp3` | FxLayer (unit created) |
| SAM built | `sam-built.mp3` | FxLayer (construction complete) |

### Waiting for approval, sound files included but NOT wired up

| Event | Sound file | Notes |
|-------|-----------|-------|
| Missile Silo built | `silo-built.mp3` | Waiting for Approval |
| SAM shoot | `sam-shoot.mp3` | Waiting for Approval |
| SAM hit | - | Waiting for Approval, no sound file assigned |
| Warship sunk | - | Waiting for Approval, no sound file assigned |
| Warship shoot | - | Waiting for Approval, no sound file assigned |

### Not done, no sound files exist yet

| Event | Notes |
|-------|-------|
| Looted player | "Not sure if needed" |
| Invaded | - |
| Ship invasion incoming | - |
| Ship sent | - |
| Menu theme song | - |
| Ambience | "Not sure if needed" |

## Test plan
- [x] Start a private game and launch atom/hydrogen/MIRV nukes, verify
launch and detonation sounds
- [x] Build structures (city, port, defense post, SAM), verify build
completion sounds
- [x] Build a warship, verify warship built sound
- [x] Receive an alliance request, verify alliance suggested sound
- [x] Break an alliance, verify alliance broken sound
- [ ] Receive a chat message, verify message sound
- [x] Open the radial menu and click items, verify click sound
- [x] Disable visual FX in settings, verify sounds still play
- [x] Adjust SFX volume slider, verify it affects all new sounds
- [x] Verify no audio issues with rapid/overlapping events
- [x] Verify SoundManager responds to EventBus events and unsubscribes
cleanly on dispose
- [x] Verify SoundManager swallows Howler errors without crashing the
game
- [x] Verify channel limit of 8, oldest sound stopped when at cap

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

Resolves #1893

## Please put your Discord username so you can be contacted if a bug or
regression is found:
cool_clarky
2026-04-06 21:01:23 -07:00