Commit Graph
896 Commits
Author SHA1 Message Date
e05dce3437 Add crown cosmetic type (#4618)
## What

Adds **crowns** as a new cosmetic category, wired end-to-end like
flags/skins, plus a Crowns tab in the lobby cosmetics modal (#4617).

Catalog shape in cosmetics.json:

```json
"crowns": {
  "gold_crown": {
    "name": "gold_crown",
    "url": "...",
    "affiliateCode": null,
    "product": null,
    "priceHard": 5,
    "artist": "...",
    "rarity": "common"
  }
}
```

## Changes

- **Core**: `CrownSchema` + optional `crowns` record in
`CosmeticsSchema`; `crownName` in `PlayerCosmeticRefs`; resolved `crown:
{ name, url }` in `PlayerCosmetics`
- **Server**: `isCrownAllowed` in the privilege checker — requires a
`crown:*` or `crown:<name>` flare, resolves the ref to the catalog URL
- **Client**: crown selection persisted under the `crown` localStorage
key (stale/unowned selections self-clear), `crownRelationship` +
resolve/refs wiring, `"crown"` purchase type, Crowns tab in the store,
and a Crowns tab in the cosmetics modal (owned crowns + Default tile;
selecting persists and keeps the modal open)
- **i18n**: `store.crowns`, `store.no_crowns`
- Tests for schema parsing, privilege checks, and cosmetic resolution

## Not in this PR

- In-game rendering of `player.cosmetics.crown` (name pass /
leaderboards / player panel)
- API-side support (`/shop/purchase` accepting `cosmeticType: "crown"`,
granting `crown:<name>` flares, serving `crowns` in cosmetics.json)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 12:49:45 -07:00
7636f4770c Merge skin/effects pickers into one lobby Cosmetics modal (#4617)
## What

Replaces the three lobby cosmetic buttons (skin, flag, effects) with
two: **Flag** and **Cosmetics**. The new Cosmetics button opens a single
tabbed modal (`#modal=cosmetics`) with:

- **Skins** — the combined patterns + image-skins grid (from
TerritoryPatternsModal)
- **Effects** — all effect types via the tabbed effects-grid, same
layout as the Store

## Changes

- New `CosmeticsModal` and `CosmeticsInput` (lobby button previews the
selected skin/pattern, shows a "Cosmetics" label on defaults, hidden on
CrazyGames via `no-crazygames`)
- Removed `TerritoryPatternsModal`, `EffectsModal`, `PatternInput`,
`EffectsInput` and their wiring in Main.ts / index.html / LangSelector
- Selecting a skin or pattern no longer closes the modal — the tile
highlight moves to the new selection (matches the Effects tab behavior)
- i18n: added `cosmetics.title/button_title/search`; removed the keys
orphaned by the deleted components

## Notes

- Deep links to the old `#modal=territory-patterns` and `#modal=effects`
no longer resolve
- A follow-up PR (crowns cosmetic) stacks on this branch

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 12:39:56 -07:00
crunchybbbandGitHub 99e6b8ac50 Adds impassable terrain to Korea Map (#4604)
> **Before opening a PR:** discuss new features on
[Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small
improvements as
[issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose).
You must be assigned to an `approved` issue — unsolicited PRs will be
auto-closed.

**Add approved & assigned issue number here:**

Resolves #4603 (issue number)

## Description:

Korea was supposed to be a two teams country map, but it released for
v32 before impassable terrain existed. Currently the Chinese portion of
the map covers far too much area, making it cannot be used for two teams
and making it not a true Korea map. The PR will convert the china and
russia portions to impassable terrain.

- Japan is still on the map to serve as a "island base" or something
- One missing south korean province equivalent was added
- The map was totally redone (because i deleted the original qgis file
for korea) so the terrain and map size is slightly different but it
should not be too easy to notice
- Adds "Countries" category

<img width="1089" height="2189" alt="image"
src="https://github.com/user-attachments/assets/82c8729f-cc10-4451-890e-27f02cfde919"
/>
<img width="367" height="579" alt="Screenshot 2026-07-13 174100"
src="https://github.com/user-attachments/assets/e24ad971-c2c5-4791-b092-74e0ff90cefc"
/>


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

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

DISCORD_USERNAME crunchybbbbb
2026-07-14 14:11:32 -07:00
RickD004andGitHub 0cbb0a0b19 Standarize the SVG of flags in flags folder, and other flag fixes (#4605)
## Description:

ALL OF THESE FLAG FILES ARE ALREADY IN GAME. NO NEW FLAGS WERE ADDED. 

Standarizes the SVGs of multiple flags. Multiple flags had issues like
not having black borders, or having borders that were too thick:

armenian SSR, azerbaijan ssr, byelorussian SSR, emirate of asir, emirate
of granada, estonian ssr, far Eastern republic, gwent, karelo-finnish
ssr, kirguiz ssr, lithuanian ssr, lower silesia, macedonia, ma clique,
moldavian ssr, Nepal, polish lithuanian Commonwealth, seville, taiping
heavenly, tannu tuva, upper silesia

renamed some flags in the menu to more common and accurate names:

holy see -> vatican
habsburg Austria -> austrian empire
circassia -> Adygea
macedonia -> macedonian empire

alkebulan, kingdom of Portugal and aztec empire used fictional or flags
that were not public domain, so they are replaced with public domain and
historical accurate ones. Countries.json also gets some missing flags,
these flags are fairly generic so they were probably accidentally left
out.

This change is so that flags have consistent designs for stuff like
using them for nations in maps

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

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

tri.star1011
2026-07-14 14:10:44 -07:00
376061793a Handle ranked play limits in the client (#4602)
## Summary

Client-side handling for the new server-enforced ranked play limits
(infra#427): free players get a lifetime allowance of ranked matches,
then a small daily allowance; subscribers on tiers with the
`unlimitedRanked` entitlement are never limited.

- **Queue rejection**: the matchmaking socket closing with code 1008 and
reason `ranked_limit_reached` now renders a limit-reached view in the
matchmaking modal (message + "Get unlimited ranked" button that opens
the store on the subscriptions tab) and does **not** auto-reconnect.
Auth-failure 1008 (`Invalid session`) is disambiguated by the reason
string and keeps the existing reconnect-with-refreshed-token path; code
1000 ("replaced by another tab") is unchanged. Both 1v1 and 2v2 go
through the same modal, so both queues get identical handling.
- **`/users/@me`**: added required `player.unlimitedRanked: boolean` to
`UserMeResponseSchema`.
- **cosmetics.json**: added required `unlimitedRanked: boolean` to
`SubscriptionSchema`; store subscription tiles now show an "Unlimited
ranked" perk line for tiers that include it.

Display copy avoids hardcoding the limit numbers since the server
constants may be tuned.

## ⚠️ Deploy ordering

Both new schema fields are **required**, so this must ship **after** the
API starts serving them (infra#427). Against the current API,
`/users/@me` parsing fails (players appear logged out) and a catalog
without the subscription field fails the whole cosmetics parse.

## Testing

- Schema tests for both new fields (present / rejected-when-missing) in
`tests/ApiSchemas.test.ts` and `tests/CosmeticSchemas.test.ts`; full
suite, tsc, ESLint, Prettier all pass.
- Verified in the running app (headless Chromium): forced the modal into
the limit-reached state, confirmed the rendered view, and confirmed the
upsell click closes matchmaking and opens the store on the subscriptions
tab.
- Not verifiable locally (needs deployed infra#427): the real close
frame, the midnight-UTC reset, and the perk line against a real catalog
— covered by the handoff QA checklist.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 19:55:40 -07:00
3a5fba2e12 Add 2v2 ranked matchmaking (#4596)
## Description:

Implements 2v2 ranked matchmaking end-to-end against the matchmaking
API's 2v2 queues (API PR #419): core team pinning, the server's second
checkin loop and game creation, and the client UI.

## Core — deterministic team pinning

The matcher's assignment specifies exactly who plays with whom (`teams:
[[a,d],[b,c]]`), but team assignment previously only did clan/friend
balancing and could scramble the ELO-balanced split.

- `PlayerSchema`/`PlayerInfo` gain an optional **`teamIndex`** — a
server-stamped index into the game's team list, part of `GameStartInfo`
so it's identical on every client (same category as `clanTag`/`friends`,
which already feed deterministic team assignment).
- `assignTeams` honors pins **unconditionally** — before clan/friend
grouping and past `maxTeamSize` (the matcher's balancing is
authoritative) — and seeds the counts that balancing of any unpinned
players sees. Pinned players still participate in the friend graph, so
an unpinned friend is pulled toward a pinned player's team.
- publicIds never enter core: the game server resolves publicId →
teamIndex per client at game start.

## Server

- **One checkin long-poll per mode.** Both loops send `mode` explicitly
(the API deployed ahead of the client, so no omit-for-back-compat
needed).
- **`get2v2Config()`**: Team mode, `playerTeams: 2`, `maxPlayers: 4`,
always-compact map, donations enabled (matching public team games),
`rankedType: "2v2"` (the API's 2v2 ingestion has shipped; `RankedType`
gains `TwoVTwo`).
- **The assignment payload is now used** (it was previously discarded):
`players` → `allowedPublicIds` so only the matched accounts can take the
slots (also hardens 1v1), and `teams` → `teamIndex` stamps at game
start. A malformed assignment logs a warning and falls back to creating
the game without pins rather than stranding matched players.
- The 3-clients-per-IP cap on public games applies to matchmade games
too (an allowlist doesn't stop one person multi-tabbing multiple
accounts). It is now skipped in dev, where local testing (multi-tab, the
4-player e2e) is inherently same-IP — matching the existing dev/prod
gating of Turnstile and the duplicate-account kick.

## Client

- Ranked modal's 2v2 card is enabled; it passes the mode through
`open-matchmaking` (dispatchers without a detail — homepage button,
requeue URL — still mean 1v1).
- Matchmaking modal joins with `&mode=1v1`/`&mode=2v2`, shows a 2v2
title (`matchmaking_modal.title_2v2` in en.json), and shows the real 2v2
ELO from the new `leaderboard.twoVtwo` field in `/users/@me` (the ranked
modal's 2v2 card does too).
- WinModal shows requeue for any ranked game and carries the mode back
into the right queue (`/?requeue=2v2`).
- 2v2 ranked stats surface in the player stats tree (labeled via
`player_stats_tree.ranked_2v2`).

## Harnesses (`tests/matchmaking/`)

- Contained: the fake server captures the `mode` query param; asserts
each queue sends its mode explicitly. **10/10.**
- E2E: `MM_MODE=2v2` runs four real browser players through the real
local worker's 2v2 queue and rides the flow into the started game.
Asserts same gameId for all four, the 2v2 config, allowlist admission,
and a **deterministic 2 vs 2 in-game split read from each client's
GameView** (the software-WebGL gate is spoofed in test pages only).
**8/8.** 1v1 e2e still **6/6.**

## Verification

- `npm test`: 2,053 tests pass, including 7 new (6 `assignTeams` pinning
unit tests + a full-game pinned-split test through `setup()`).
- `npx tsc --noEmit`, ESLint clean.
- Live e2e against a local `wrangler dev` API worker: 1v1 (6/6) and 2v2
(8/8) as above.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


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

(UI changes — the ranked modal's 1v1/2v2 cards — were verified with
before/after screenshots in the live app during development.)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 15:22:52 -07:00
Zixer1andGitHub 290d6922eb feat(playlist): add the Doomsday Clock to the public modifier rotation (#4589)
## Description:

Adds the anti-stall **Doomsday Clock** to the public "special" modifier
rotation, so it rolls into public games like the other spice modifiers,
with a lobby badge.

- `MapPlaylist`: new `isDoomsdayClock` ticket in `SPECIAL_MODIFIER_POOL`
(weight 4, ~20% of special games). When rolled, enables `doomsdayClock`
at a speed picked per game (slow/normal/fast/veryfast).
- `PublicGameModifiers` (type + schema): `isDoomsdayClock` flag drives
the badge.
- `getActiveModifiers` + `en.json`: a "Doomsday Clock" badge/label.
- Test covering the badge wiring.

## 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
2026-07-13 11:11:11 -07:00
Zixer1andGitHub d07f7d46dd feat(highlight): replace the small-player highlight toggle with a strength slider (#4588)
## Description:

Replaces the "Highlight small players" on/off toggle with a **0-500%
Strength slider** controlling the glow's **width** (0 = off, 100% = the
previous default).

- `UserSettings`: `highlightSmallPlayers()`/toggle →
`highlightGlowStrength()`/set, float-backed, clamped to [0, 5].
- `SmallPlayerGlowPass` takes the strength via a setter (pushed by
`ClientGameRunner` on the settings-changed event), so it stays a pure
consumer and the slider still updates live while the settings modal has
the sim paused. Width = a non-linear blur-iteration count; intensity
compensated so wider stays about as bright.
- `MapRenderer` persists the strength across a WebGL context restore.
- `SettingsModal`: the toggle row becomes a range input.

Behavior note: with the toggle gone the glow **defaults ON at 100%**
(old toggle defaulted OFF); can default to 0 (opt-in) if preferred.

## 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
2026-07-13 11:07:22 -07:00
e76b34be22 Handle matchmaking socket close codes per API contract (#4595)
## What

Implements the close-code contract from the matchmaking API handoff (API
PR #419) in the matchmaking modal, plus two runnable integration
harnesses. Previously `onclose` only logged, so an API deploy while
queued left the player on the "searching" spinner forever (the queue is
in-memory on the API worker).

| Close | Behavior |
| --- | --- |
| 1008 `Invalid session` | Reconnect and re-send `join` —
`getPlayToken()` refreshes an expired token internally, so the rejoin
carries a fresh JWT |
| 1000 `Replaced by newer connection` | Another tab/window took the
queue slot: show a message (new `matchmaking_modal.replaced` string) and
stop. No retry |
| Any other close before assignment | Server restart/deploy: reconnect
and re-send `join`, with exponential backoff (1s doubling to a 15s cap)
|

Intentional closes (user backs out of the modal, assignment received)
don't reconnect. A pending 2s join timer from a previous socket is
cleared before each reconnect so it can't fire on the new socket.

## Test harnesses (`tests/matchmaking/`)

- **`npm run test:matchmaking`** (contained): drives the real modal in a
headless browser against a fake in-process matchmaking server speaking
the documented protocol; covers the whole close-code table over real
WebSockets. Needs only `npm run dev`.
- **`npm run test:matchmaking:e2e`**: real integration against the API
worker on `localhost:8787` — two browser players join the real queue,
the dev game server receives the checkin assignment, and both clients
end up in the same created game.

## Why now

This is required by the matchmaking API's client contract independent of
the upcoming 2v2 work ("clients must already handle unexpected close →
reconnect and rejoin"). The rest of the 2v2 integration is planned for
the next version.

## Verification

- Contained harness: 8/8 checks pass (join, 1006 reconnect+rejoin, 1008
rejoin with fresh token, assignment, replaced → message + no retry,
intentional close → no retry/no message).
- E2E harness against a local worker: 3/3 — both players matched into
the same game, game created via checkin and joinable by both.
- `npm test` (all 2,046 pass), `npx tsc --noEmit`, ESLint clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 10:55:15 -07:00
RickD004andGitHub 558f2e20db Adds Gulf of Guinea map (#4590)
## Description:

Adds Gulf of Guinea map to the game:
https://en.wikipedia.org/wiki/Gulf_of_Guinea

Final map of the "pirate maps" for v33 , alongside Irish Sea and Levant.
The map is designed so that all the trade from the mainland, which
covers 2 of the four corners of the "square" (inverse L) passes through
3 small islands, which although small in size they will super-boost
pirates in that zone. The mainland is otherwise fairly lineal, playing
like maps like Conakry.

Also some sub-saharan african representation

The map is decorated with Impassable terrain in its southern border, and
has additional nations for Hvn and Solo games


https://github.com/user-attachments/assets/9c9fd8c0-fefb-46c4-8720-55e90401681e

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

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

tri.star1011
2026-07-13 09:17:02 -07:00
d76691372c Feature/reuse private lobby (#4536)
**Add approved & assigned issue number here:**

Resolves #4476 

## Description:

Lets a private-lobby host reuse the same group for **back-to-back games
without
re-sharing the invite link**.

**Flow:** in a private game, the host clicks a **"New lobby"** button in
the
top-right bar (next to pause). The game's **server** creates a fresh
private
lobby (same creator, default settings) and broadcasts its id to everyone
still
connected. Non-hosts get a one-click **"Join"** banner at the top of the
screen;
the host is taken straight back to the host view for the new lobby. The
chain
can repeat indefinitely.

### Key design decision: the server creates and broadcasts the successor
lobby

The successor lobby is minted by the **finished game's server**, not the
host's
browser. The old game's server is the only thing still connected to
every player, so it has to be what announces the new
lobby; because it also *creates* that lobby, the id everyone is
redirected to is
**authoritative**. A real lobby the server just made for the
authenticated
creator, not an id a client handed it to trust and fan out. The request
is
**creator-only** and **idempotent** per game, and every successor is
wired the
same way, so the group can keep playing game after game.

### How it works

1. Host clicks "New lobby" (host + private only) → confirm dialog → the
client
   sends a `create_next_lobby` message.
2. The game server verifies the sender is the lobby creator, mints a
successor
private lobby on the same worker, stores it (idempotent), and broadcasts
a
   `new_lobby` message with the new id to all connected clients.
3. Each client reacts: the host is navigated to the new lobby's host
view
(`/…/game/<id>?host`); everyone else sees a dismissible "Host started a
new
   lobby — Join" banner that navigates to the join URL in one click.

Two new Zod wire messages in `src/core/Schemas.ts` (`create_next_lobby`,
`new_lobby`) carry the request and the broadcast.

### Screenshots

<table>
  <tr>
    <td width="50%" align="center" valign="top">
<img width="220" alt="In-game New lobby button"
src="https://github.com/user-attachments/assets/9a4d4425-a7f6-4b3a-9c4f-9205300b1e5b"
/><br />
<sub><b>1.</b> In-game <b>New lobby</b> button (top-right, next to
pause) — shown only to the host of a private lobby</sub>
    </td>
    <td width="50%" align="center" valign="top">
<img width="320" alt="Confirmation dialog"
src="https://github.com/user-attachments/assets/fa023f5a-58e8-439b-8899-5150235a1e8c"
/><br />
<sub><b>2.</b> Confirmation so a stray click doesn't pull everyone into
a new lobby</sub>
    </td>
  </tr>
  <tr>
    <td colspan="2" align="center">
<img width="100%" alt="Join banner for non-hosts"
src="https://github.com/user-attachments/assets/7e5ef490-aa7e-4449-add9-e857fe273bde"
/><br />
<sub><b>3.</b> Everyone else gets a one-click <b>Join</b> banner at the
top of the screen</sub>
    </td>
  </tr>
  <tr>
    <td colspan="2" align="center">
<img width="330" alt="Host view for the new lobby"
src="https://github.com/user-attachments/assets/9920b070-4ed3-41f8-9345-78778b4648a7"
/><br />
<sub><b>4.</b> The host lands back in the host view for the brand-new
lobby</sub>
    </td>
  </tr>
</table>


### Design Decisions

**A. The server creates & broadcasts the successor, not the client.**
The finished game's server mints the successor and broadcasts its id.
Why this
and not "host's browser calls `POST /api/create_game`, then asks the
server to
relay the id"?
- The broadcast id is **authoritative/verified**: it's a real lobby the
server
just created for the **authenticated** lobby creator (creator identity
comes
from the JWT the game already holds), not an arbitrary id a client hands
the
  server to fan out to everyone.
- The **old game server is the only thing still connected to all the
players**,
so it must be the one to broadcast. Having it also create the lobby
keeps it
to one authoritative round-trip instead of "client creates, then client
asks
  server to trust an id it didn't make."
- The server can **authorise** (only the creator) and stay
**idempotent**.

**B. The successor starts with default settings (not a copy of the old
game).**
A deliberate scope choice. The host lands in the normal host view and
reconfigures. Copying the exact config would mean reverse-mapping every
`GameConfig` field back into the host-modal controls, which I thought
would be out of scope for this PR. Same **creator**
is preserved; same **settings** intentionally is not.

**C. It's a brand-new lobby, not the same game resurrected.**
This directly follows @evanpelle's guidance on the issue: *"A 'lobby' is
really
just a game that hasn't started yet. So making a persistent lobby isn't
really
possible. I think instead having a simple way to transfer players to a
new lobby
is probably the way to go."* A `GameServer` runs exactly one game
(start → end → archive), so rather than reworking that lifecycle to
resurrect the
old game, the server spins up a fresh successor lobby and transfers the
group
into it, which is also why the feature is framed as "reuse the group,"
not
"reuse the game object."

**D. The host returns via a `?host` URL flag + full reload ("attach
mode").**
Navigating to a normal join URL (`/game/<id>`) always lands you in the
**join**
view, which has no Start button. So the host can't just use the join
URL. The
`?host` flag routes the creator to the **host view** instead
(`Main.handleUrl` → `HostLobbyModal` in "attach" mode, which binds to
the
existing lobby id and skips creating a new one). A full reload is used
because
it cleanly tears down the finished game and mirrors the existing
win-screen
"Requeue" button's `window.location.href` pattern.

**E. Each successor can spawn its own successor (recursive factory).**
The first version only chained **one** generation. A spawned lobby had
no
factory of its own, so the *second* "New lobby" click did nothing
(button just
greyed out). Fixed by `wireSuccessorLobby` (a small dependency-injected
helper)
that wires every successor the same way. This is the whole point of the
issue
("back-to-back games"), so it has its own regression test.

**F. Private-only.**
The successor factory is installed **only** on the private `POST
/api/create_game` path in `Worker.ts`. Public games (scheduled by the
master)
and singleplayer never get a factory, so `handleCreateNextLobby` is a
no-op for
them. The client button is also gated on `isLobbyCreator &&
isPrivateLobby`.

**G. In-game button + confirm; the win-modal button was removed.**
An earlier version put the "New lobby" button on the win screen. I moved
it to
the in-game bar so the host can reuse the lobby **at any time** (without
dying
or waiting for the game to end), and added a **confirm** (matching the
adjacent
Exit button) so a stray click next to pause/exit doesn't yank everyone
into a
new lobby. The win-screen button became redundant and was removed.


### Testing

- **Unit tests:** wire-message schema round-trips
(`tests/NewLobbyMessages.test.ts`); the server handler — authorisation,
  broadcast, idempotency — against a real `GameServer`
(`tests/server/CreateNextLobby.test.ts`); and successor **chaining**
across
  multiple generations (`tests/server/SuccessorLobby.test.ts`).
- **Full suite:** `npm test` passes — **1782 tests across 154 files**.
- **Manual:** created a private lobby with multiple clients and played
consecutive games via the button; verified non-hosts get the Join
banner, the
host lands back in the host view, and the chain works for 3+ games in a
row.

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

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

MushroomLamp

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:51:49 -07:00
a794eca4d6 Show USD-equivalent value in cosmetic info tooltip (#4571)
## Summary

fixes #4171

The question-mark info tooltip on store cosmetics now shows the item's
equivalent USD value, but only when both:

1. The item has **no product** (i.e. it can't be purchased directly with
money), and
2. It **is purchasable with plutonium** (`priceHard`) — caps-only items
show nothing.

The value is computed at the fixed rate of 20 plutonium = $1.00 (same
rate as the custom currency card) and rendered as e.g. `Value: $2.50`.

## Changes

- `CosmeticButton.ts` — compute `usdValue` (`priceHard / 20`) when the
item has no product and has a plutonium price; pass it to
`<cosmetic-info>`
- `CosmeticInfo.ts` — new optional `usdValue` property rendered as a
tooltip line between the ad-free and color lines
- `en.json` — new `cosmetics.usd_value` key with a `{usd}` placeholder
so translators can position the amount

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:40:09 -07:00
RickD004andGitHub 1cd36325ac Adds Irish Sea map (#4546)
## Description:

Adds map of the Irish Sea: https://en.wikipedia.org/wiki/Irish_Sea

A relatively small-medium size map of 970k tiles, centered around Isle
of Man (slightly modified to be more centered), surrounded in all sides
by land.

Since all trade will go around the island, this will create crazy strong
pirate players, which have become popular thanks to Youtubers and the
Tradeship buff in v32.

19 default nations, with 43 extra nations for a total of 62, for the
Humans vs Nations gamemode and Solo games.


https://github.com/user-attachments/assets/18e0ee2c-8d7f-4ce7-813c-2e7c64d75acc

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

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

tri.star1011
2026-07-10 14:19:55 -07:00
crunchybbbGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
c25f898809 Adds Scandinavia map (#4561)
> **Before opening a PR:** discuss new features on
[Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small
improvements as
[issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose).
You must be assigned to an `approved` issue — unsolicited PRs will be
auto-closed.

**Add approved & assigned issue number here:**

Resolves #4543 

## Description:

Adds Map of Scandinavia. This is highly requested and I'm surprised this
map doesnt exist already. The layout is based on the Territorial
version.
Land area: 2.47M pixels
Nations: 24, with additional nations totaling to 62.

<img width="2006" height="2243" alt="image"
src="https://github.com/user-attachments/assets/a959acd3-7871-4d94-8722-b0b69defa9db"
/>
<img width="551" height="590" alt="Screenshot 2026-07-08 162324"
src="https://github.com/user-attachments/assets/e4a49073-04dc-416d-841a-ab945a40d260"
/>


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

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

DISCORD_USERNAME crunchybbbbb

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-07-10 14:18:08 -07:00
bf61f5847b feat(rewards): claimable subscription rewards UI (#4566)
## Summary

Subscription perks (signup bonus + daily soft/hard currency) now land as
**unclaimed rewards** the player must explicitly claim, instead of
crediting balances directly (API side: openfrontio/infra#409). This PR
adds the client UI:

- **`RewardsPanel`** — shared Lit element listing pending rewards
(currency icon, `+amount`, server `note` with translated fallbacks for
known reasons), with per-reward **Claim** and **Claim All**. Both claim
endpoints return post-claim balances, so the wallet updates without
re-fetching `/users/@me`.
- **Account modal** — renders the panel above the subscription panel
(rewards survive subscription lapse, so it doesn't depend on an active
sub).
- **`RewardsModal`** — popup at login when rewards are pending. Reuses
`RewardsPanel`, auto-closes when the last reward is claimed. Only opens
on a clean homepage load (`pathname === "/"`, empty hash) so it never
pops over join links, `#modal=...` deep links, or the Stripe
`#purchase-completed` return — that flow reloads clean, so the signup
bonus popup appears right after purchase.
- **Schemas/API** — `RewardSchema` + `rewards[]` on `/users/@me`
(optional for older API versions), `claimReward` / `claimAllRewards` in
`Api.ts`. Reward `id`/`amount` stay opaque bigint strings; a 404 on
single claim means "already claimed elsewhere" (double-click / second
device) and re-syncs instead of erroring.

## Screenshots

Login popup (also embedded in the account modal):

- Panel: currency icon + amount + note per row, Claim / Claim All
buttons
- Amounts formatted via `BigInt` (can exceed `Number.MAX_SAFE_INTEGER`)

## Test plan

- [x] `npm test` — 161 tests pass, including new coverage for
`RewardSchema`, `/users/@me` with/without `rewards`, and both claim
response shapes
- [x] `tsc --noEmit`, ESLint, Prettier clean
- [x] Verified in the running app (Playwright): panel renders in the
account-modal style; single claim against a stubbed endpoint credits
balances and removes the row; Claim All empties the list and auto-closes
the popup; body scroll restored

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:21:52 -07:00
8e5f27949f feat: marketing email consent UI (post-login prompt + account settings) (#4554)
Resolves #4029

## Description:

Client-side marketing-email consent capture — the client half of #4029.
It consumes the already-merged API (`GET /users/@me` marketing-consent
state + `POST /marketing/consent`); **no backend changes**.

**Three surfaces:**

- **Post-login prompt** — a small, non-blocking toast docked top-right,
shown once after login when the player's consent is undecided
(`no_response`) and a verified email is on file. Yes / No thanks /
dismiss all record a decision, and it never re-nags.
- **Account → "Account Settings" tab** — a persistent on/off toggle to
change or withdraw consent at any time (the GDPR change/withdraw path).
- **No-email state** — when the account has no verified email, the tab
offers to bind one (magic link to a plain email — the backend's
`new-association` path — or link Google) so the player can subscribe.

Styling matches the game's existing panels (`bg-surface`,
`border-white/10`, `shadow-[var(--shadow-malibu-blue)]`, malibu-blue
accent), reusing `o-button` and the account modal's existing email
field/handlers.

**Changes:**

- New `<marketing-consent-toast>`
(`src/client/MarketingConsentToast.ts`), mounted in `index.html`,
registered in `Main.ts`.
- `AccountModal.ts`: new "Account Settings" tab (toggle + bind-email
state), optimistic with revert-on-failure.
- `Api.ts`: `setMarketingConsent()` → `POST /marketing/consent`,
invalidates cached `/users/@me`.
- `ApiSchemas.ts`: optional `player.marketingConsent { consented,
hasEmail }` on `UserMeResponse`.
- `en.json`: `marketing_consent.*` + `account_modal.marketing_*` /
`tab_settings`.

## Please complete the following:

- [x] I have added screenshots for all UI updates <!-- attaching in a
follow-up comment -->
- [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

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

Iamlewis

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:13:37 +00:00
RyanandGitHub 921892e941 "stats" not "ranking" (#4560)
## Description:

people confused what "ranking" means on the games stats modal
<img width="606" height="255" alt="image"
src="https://github.com/user-attachments/assets/ac695d74-c5b2-4940-b3af-abe3196d2f00"
/>


also sorta helps with the overflow

before:
<img width="350" height="334" alt="image"
src="https://github.com/user-attachments/assets/8a788b79-2d76-4914-b14f-48838494fae5"
/>

after:
<img width="357" height="332" alt="image"
src="https://github.com/user-attachments/assets/164515c7-8b22-45c9-bd11-b5b47d4f17ed"
/>

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

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

w.o.n
2026-07-09 14:45:06 -07:00
SpeakIsntThereandGitHub c5b2d60661 Germany (#4559)
Resolves #4558 

## Description:
Sets 1 pixel on the Oder river to be water to allow trade to flow
through
Sets 1 pixel on the left side of the Oder to be land and not impassable

<img width="730" height="1197" alt="Screenshot 2026-07-09 203514"
src="https://github.com/user-attachments/assets/5f711ea3-0e92-467b-b030-1099c6c8a97e"
/>


## 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
 
## Please put your Discord username so you can be contacted if a bug or
regression is found:

Lengy_
2026-07-09 13:40:53 -07:00
RyanandGitHub 70d301fbb9 custom amount ui prettier (#4556)
## Description:

fixed font, 
sizing, 
back box wrapping, 
remove "buy for" 
centre the text by removing the ^ v arrows

before:
<img width="939" height="491" alt="image"
src="https://github.com/user-attachments/assets/3c747341-5238-47e8-b455-ea5827669e75"
/>



after:
<img width="936" height="746" alt="image"
src="https://github.com/user-attachments/assets/4b64f34b-98e4-4519-85bd-1f5c2799babd"
/>

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

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

w.o.n
2026-07-09 12:37:46 -07:00
SpeakIsntThereandGitHub 901b271acf Add Germany Map and state flags (#4513)
Resolves #4390 

## Description:

- Adds new map, Germany, filed under the New and Europe categories, for
use in Singleplayer and Custom lobbies.
- Adds map to lobby rotation.
- Adds new flags to represent all 16 Federal German states (including a
variant of Bavaria), all to the ISO standard (DE-XX)

Map Info:
Size: 1548x1519, 2,351,412 total. 451,764 water pixels (19.2%), 826,627
impassable pixels (35.2%)
Nations: 16, all of which are Federal German States

Yes, the size has changed and also has impassable terrain added now

<img width="652" height="887" alt="image"
src="https://github.com/user-attachments/assets/6499382e-f073-4ef8-956c-4e80ef09d4b8"
/>
<img width="917" height="905" alt="image"
src="https://github.com/user-attachments/assets/d9c7a532-c2fd-415a-a51d-709be3b43aae"
/>
<img width="922" height="1140" alt="image"
src="https://github.com/user-attachments/assets/fca34d84-414c-4b3f-91ec-f0a21883caa6"
/>

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

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

lengy_

Sorry if I've done anything wrong or the files are out of date, I made
the map a week ago, my fork is a mess and I'm still quite new to how
exactly GitHub works.
2026-07-08 15:26:53 -07:00
Zixer1andGitHub f5ef306d99 feat(client): highlight small players with a pulsing glow (#4525)
## Description:

Adds a **"Highlight small players"** client setting (in the in-game
Settings menu). When it's on, human players holding **0.2% or less of
the map** get a soft red glow that breathes (a pulsing aura) around
their territory, starting **one minute into the game**. This makes
near-eliminated players easy to spot on a busy map, even when their
territory is scattered in fragments or sitting under structures.

How it works:

- **Purely client-side**, no simulation or determinism impact.
`SmallPlayerGlowPass` renders a tile-space bloom: extract a sub-tile
mask of the small players' tiles, run a separable Gaussian blur, then
composite the soft aura over the map additively. It's camera-independent
(no shimmer when panning/zooming), so scattered tiles blur into one
clean halo. The aura breathes: its intensity fades fully to 0 at the
trough for clear contrast.
- The set is recomputed each tick in `WebGLFrameBuilder`: alive human
players with `tilesOwned / (landTiles - fallout) <= 0.002` (0.2%),
suppressed during the spawn phase and the first minute.
- Backed by a persisted `UserSettings` flag, toggleable live from the
in-game Settings modal (with a new, distinct icon).
- Drawn after the structure passes so buildings can't hide it.
- Mirrors the existing FalloutBloom pipeline and reuses the shared blur
shader and render-target helpers rather than duplicating them. Tunable
via `render-settings.json` (`smallPlayerGlow`: color / alpha /
pulseSpeed).

UI (glow fades in and out):
<img width="474" height="334" alt="image"
src="https://github.com/user-attachments/assets/2b94f292-2cbb-43d3-82fb-f274d1afdedf"
/>
<img width="976" height="400" alt="image"
src="https://github.com/user-attachments/assets/f1b972d7-e046-4f7a-ab43-da1eb758c7b5"
/>


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

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

zixer._
2026-07-08 12:30:37 -07:00
Zixer1andGitHub 050c7604e8 feat(alliances): custom alliance duration lobby control (#4522)
## Description:

Replaces the "Disable alliances" toggle in the host and single-player
lobbies with a "Custom alliances" control: a toggle plus a minutes input
(0 to 15, step 1).

- 0 minutes disables alliances, same behavior as the old toggle.
- 1 to 15 sets the alliance duration in minutes.

How it works:

- Adds one game-config field, `customAllianceDuration` (minutes).
- `Config.allianceDuration()` uses it when set (and falls back to the
existing 5 minute default), and `Config.disableAlliances()` returns true
when it is 0.
- The legacy `disableAlliances` boolean is still read, so older/archived
configs keep working.
- Validation reuses the existing `parseBoundedIntegerFromInput` and
`toggle-input-card` helpers, so it behaves like the other numeric lobby
options (spawn immunity, max timer).
- The join-lobby screen shows "Alliances: {x}m", or "Alliances:
Disabled" at 0.

<img width="279" height="145" alt="image"
src="https://github.com/user-attachments/assets/5a608e18-3811-4eef-a3a6-9344aaf667fe"
/>
<img width="270" height="152" alt="image"
src="https://github.com/user-attachments/assets/9d8a4d30-51e7-4e82-8ce0-121b12af1c61"
/>
<img width="270" height="144" alt="image"
src="https://github.com/user-attachments/assets/a65b20fb-0db5-4657-a964-880fad7c864e"
/>


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

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

zixer._
2026-07-08 12:13:35 -07:00
d3ad1f51bd fix(crazygames): guest username on logout, hide fullscreen, in-game pop-ups (#4538)
Addresses four requests from the CrazyGames platform team.
Supersedes #4520 (closed when its branch was renamed to `crazygames`).

## 1. Username reverts to guest on CrazyGames logout
When a player logged into their CrazyGames account and then logged out,
the username stayed the CrazyGames name. It now reverts to a fresh guest
(`AnonXXX`) name. A `crazyGamesLoggedIn` flag guards this so it only
fires on a real login→logout transition (not on the initial "not logged
in" state, which would otherwise wipe a stored name).

## 2. Fullscreen button hidden on CrazyGames
CrazyGames provides its own fullscreen control in the game frame, so our
in-game fullscreen button is now hidden when running on CrazyGames (and
unchanged everywhere else).

## 3. Native browser prompts → in-game pop-ups
The `showInGameConfirm()` / `showInGameAlert()` helpers (promise-based,
callable from non-Lit contexts like WebSocket/popstate handlers) drive
the existing shared `<confirm-dialog>` component, so styling stays
consistent with the rest of the app. Every native `confirm()`/`alert()`
shown **during gameplay** now uses it:
- Exit-game confirmation (right sidebar + browser-back path)
- Kick-player confirmation
- Host-left notice (dispatches leave-lobby only after dismiss,
preserving the old blocking UX)
- Connection-refused notice (also removes a stale `// TODO: make this a
modal`)

## 4. `gameplayStop` when Settings menu / pop-up is open
- The pop-up helpers report `gameplayStop()` while shown and
`gameplayStart()` on dismiss.
- Settings + Graphics Settings modals now report `gameplayStop` whenever
the menu is open — previously it only fired when the modal *also* paused
the sim (singleplayer / lobby-creator), so regular multiplayer players
never triggered it. Also fixes graphics-settings so gameplay resumes
correctly on close for those players.

## Scope
Per request, this covers **in-game dialogs only**. The main-menu native
alerts (store/checkout, account, subscriptions, friends, login) were
intentionally left as-is.

## Testing
- `tsc --noEmit`, `eslint`, and `prettier --check` all pass.
- Verified the confirm and alert pop-ups render correctly in the running
app (shared `<confirm-dialog>`, danger/warning variants), resolve their
promises, and tear down their portal.
- CrazyGames-iframe-only behaviors (username revert, `gameplayStop`,
fullscreen hiding) are verified by code inspection — they require
running inside the actual CrazyGames frame.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 12:02:37 -07:00
f78b5c42c9 Update Map Outlines - No Resizing (except oceania) (#4509)
Resolves #4450 


## Description:

- Cosmetic updates to some existing maps using the new impassable
feature. This PR is focused on maps that require no resizing (no
increase in size) to accommodate decorative impassable map outlines. A
future PR will focus on maps that require resizing.
- Form follows function. Established "metro" map style by removing
excessive water off of maps, but going no further. Edits are simple and
repeatable, but also flow with the terrain of the map. This is NOT about
the removal of rectangular maps.
- Show off impassable feature on existing maps without changing game
play.

https://youtu.be/e1gsvjl-CGE

Maps Updated
baja
bosphoruss
didier
gateway
golfofstlawrence
iceland
juandefuca
lisbon
newyork
niledelta
northwestpass
oceania
san fran
south america
straitofmalacca
surrounded
tourney 1-4



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



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

PlaysBadly

---------

Co-authored-by: RickD004 <realtacoco@gmail.com>
2026-07-07 20:35:36 -07:00
blonandGitHub 965c4e8a87 1fix(fourislands): fill landlocked lake on NW island to fix port and b… (#4532)
Resolves #4531

## Description:

Fills the 462-pixel landlocked lake on the eastern shore of the
North-West island on the Four Islands map by coloring it with the
surrounding land color in image.png and recompiling the map assets. This
resolves gameplay bugs where ports built on this shore would face the
lake instead of the ocean (preventing warships from spawning into the
ocean) and transport ship routing to this shore was blocked due to the
lake being disconnected from the main ocean water component.
## Please complete the following:

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

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

blontd6

<img width="1536" height="1448" alt="image"
src="https://github.com/user-attachments/assets/f6852d6f-78f8-4ca7-b5b3-c5d4475543e0"
/>
2026-07-07 20:11:50 -07:00
NotRocketfishandGitHub a375d77539 Adds new map Branching Paths. (#4526)
It's my first (now second lol) time doing this so let me know if there's
anything off and or wack with it.

<img width="2480" height="2150" alt="Maping3wayBranches"
src="https://github.com/user-attachments/assets/e4d4281d-8024-47ee-ae5d-7f5e6d8731bd"
/>


Resolves #4524 

## Description:


Fictional / Arcade fractal tree based map running in 3 ways using the
new wall / void tiles to form a triangle. Total playable volume is
~2.65M pixels with ~545K pixels of water. (For a land volume of ~2.1M.)
Walls / void occupies a large volume of the map with ~2.68M black
pixels. (total file volume is 5,332,000 pixels.)

19 Native nations with generic names and no flags. I'm bad at that :p

Thanks to PlaysBadly for help on this!

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

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

TheGamingFish
2026-07-07 19:46:50 -07:00
Zixer1andGitHub 4ce57efbe2 Rebalance Doomsday Clock: late-game stalemate-breaker (10min grace + wave squeeze), slower troop drain, gentler-but-steeper warship attrition (#4518)
Resolves #<add your approved & assigned issue number>

## Description:

Rebalances the Doomsday Clock so it acts as a **late-game
stalemate-breaker** rather than an early-game culler, softens how fast
it removes troops and warships, and makes the HUD countdown clearer.

**Clock schedule (all presets):**
- A flat **10-minute grace** at 0% required share — the early game is
decided by combat, not the clock.
- Then a 6-wave squeeze at accelerating levels (4 / 9 / 16 / 26 / 40 /
55%) with short pauses, reaching the final 55% at each preset's cap:
**45 / 35 / 25 / 15 min** for slow / normal / fast / veryfast.
- `WaveSchedule` `rampSeconds`/`pauseSeconds` are now **per-wave
arrays**, so the curve can be shaped (gentle early, steeper late)
instead of one uniform ramp. `requiredBasisPoints` and the HUD companion
`doomsdayClockWaveState` walk the per-wave segments in lockstep.

**Troop drain:** warn window `10s → 30s`, drain eased (`2%→5%` over
`90s`), so a caught side takes ~2 minutes to wipe instead of ~1.

**Warship attrition:** warships get their own gentler start plus a
**convex** decay curve — a ship caught when its side is first doomed
lasts about as long as troops, but the rate ramps up steeply so a side
at full attrition still loses its fleet in ~2s. Adds a `curveExponent`
argument to `doomsdayClockDrain` (1 = linear, used for troops; higher =
convex, used for ships).

**Determinism:** the drain curve is **integer-only** (fixed-point power,
no floats), so the floored per-tick loss is bit-identical on every
client in the lockstep sim. The linear troop path keeps its exact
existing integer form; only the convex warship path is reshaped.

**HUD countdown clarity:** the clock readout now shows a live countdown
in both states — `Will reach 16% in M:SS` while the bar is actively
climbing, and `Starts rising to 26% in M:SS` during a pause (previously
`Next 26% in …`, which read as if it jumped there instantly). Backed by
a new `secondsToTarget` field on the shared wave state so the sim and
HUD stay in agreement. All display text goes through `translateText()` /
`en.json`.
<img width="278" height="116" alt="image"
src="https://github.com/user-attachments/assets/e0be3d2c-bb88-46be-b344-34d63e4859bd"
/>
<img width="304" height="171" alt="image"
src="https://github.com/user-attachments/assets/da74bd05-0b9a-4aec-bbf5-e7380fbda88e"
/>

**Danger skull:** while a side is below the bar in the warn window, its
on-map skull now blinks progressively faster as the countdown runs out
(accelerating to the moment the drain begins), then holds steady once it
is actually draining — a clearer "you are about to be hit" cue.

Rationale: the previous schedule removed players heavily in the first
half of a match and could leave a drawn-out endgame. Holding the clock
at 0% early keeps the opening about fighting, and concentrating its
pressure in the back half reserves it for actually breaking stalemates.
Values are tuning starting points and easy to adjust in
`DoomsdayClock.ts` / the config 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

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

<add your Discord username>
2026-07-07 19:38:36 -07:00
RickD004andGitHub 1db65a1877 Big Map PR: Adds map of Russia (#4521)
## Description:

**Adds map of Russia, large map for the largest country:** 

Yet another country map that uses impassable terrain, to go along the
China and USA maps.

Huge map with 2.5M land pixels (so that it can reach the biggest lobby
ammount, 125 players). By total area this is also one of the largest
maps, this will create fun crazy matches with massive endgames.

80+ nations/NPCs representing russian Oblasts, Republics and more, each
with their own flag (2/3 of the files of the PR are the flags 😂)

<img width="1307" height="541" alt="image"
src="https://github.com/user-attachments/assets/38c71d1c-03ed-4a75-97a3-d061f46e1b04"
/>

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

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

tri.star1011
2026-07-07 09:33:11 -07:00
0c83444dc9 feat(client): custom plutonium amount purchase in store (#4515)
## Summary

Adds a **custom plutonium amount** purchase option to the Store, wiring
the client to the new `POST /stripe/create-custom-currency-checkout`
endpoint (infra [PR
#400](https://github.com/openfrontio/infra/pull/400)). Players can buy
any integer **20–2000** plutonium, priced inline server-side at **20
plutonium = \$1.00**.

A new `<custom-currency-card>` renders as the **last tile in the Store
"Packs" tab** — a slider + number input with a live price and a "Buy for
\$X.XX" button that redirects to Stripe Checkout.

## Changes

| File | Change |
|---|---|
| `src/client/Api.ts` | New `createCustomCurrencyCheckout(hardAmount)` —
POSTs `{ hardAmount, hostname }`, returns the Stripe `url` (mirrors
`createCheckoutSession`). |
| `src/client/components/CustomCurrencyCard.ts` | New
`<custom-currency-card>` component: plutonium slider + number input,
live price, clamps to 20–2000 integer client-side, redirects to Stripe
on buy. |
| `src/client/Store.ts` | Renders the card after the fixed packs in the
"Packs" tab; drops the now-unreachable "no packs" empty state. |
| `src/client/Main.ts` | Handles `type=custom_currency` in the
`#purchase-completed` redirect (success alert + strip hash; balance
refetches from `/users/@me` on load). |
| `src/client/LangSelector.ts` | Registers `custom-currency-card` for
translation-reload re-render. |
| `resources/lang/en.json` | Adds `store.buy_for`,
`store.custom_amount`, `store.custom_currency_purchase_success`; removes
the orphaned `store.no_packs`. |

## Notes

- **No catalog product** — unlike currency packs, the custom amount is
priced inline server-side, so the card is standalone (not a
`resolveCosmetics` item).
- **Async credit** — no optimistic balance bump; the balance reflects
the Stripe webhook via `/users/@me` on return, matching the
currency-pack flow.
- Server is authoritative on bounds/rate; client-side clamping is
UX-only.

## Testing

- Full suite green: **1842 tests / 153 files**; `tsc --noEmit` and
eslint clean.
- Manually verified in the running app (headless Chromium): card renders
in the Packs tab; amount/price stay in sync across slider + number
field; clamping confirmed (5000→2000/\$100.00, 5→20/\$1.00, 750→\$37.50
— matches the pricing table).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 17:23:58 -07:00
evanpelleandClaude Fable 5 2a82b01e30 feat: auto-start listed lobbies after 5 minutes
Hosts can't sit on a public listing: setListed(true) records listedAt,
and once listedAt + HOSTED_LOBBY_AUTO_START_MS passes, GameManager's
tick arms the normal start countdown (same path as the host's Start
button, honoring startDelay). Cancelling the countdown re-arms on the
next tick; unlisting cancels the deadline and relisting starts a fresh
one. Duplicate setListed(true) calls don't extend it.

The deadline rides to the host as autoStartAt in lobby info, rendered
as an amber countdown next to the Public/Private switch in the host
modal header (hidden once the real start countdown takes over).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:11:13 -07:00
5d21be826d feat: subscriber-hosted public lobby listing (#4480)
Part of #4040 (v1 scope: listing + browser + per-subscriber limit;
custom lobby name/description left for a follow-up).

## What

Subscribers can toggle their **private lobby** to be **publicly
listed**; a browsable **"Open Lobbies"** list appears in the Join Lobby
modal. Hard limit of **one listed lobby per subscriber**, enforced
cluster-wide.

## How

**Semantics** — a listed lobby stays `GameType.Private`: the host keeps
full control and starts the game manually; the toggle only controls
visibility. The `listed` flag lives on `GameServer` (not `GameConfig`),
so it cannot be smuggled in through `update_game_config` and never
touches core/sim/records.

**Distribution** — reuses the existing public-lobby pipeline end to end:
a new `"hosted"` `PublicGameType` bucket flows worker → master IPC →
`/lobbies` websocket → `PublicLobbySocket`. Master scheduling now
iterates only `SCHEDULED_PUBLIC_GAME_TYPES` (`ffa`/`team`/`special`), so
it never sets countdowns on or schedules replacements for hosted
lobbies. Lobbies delist automatically when the game starts/fills/dies
(phase change). The broadcast fingerprint now includes browser-visible
config, so host edits (map/mode) refresh the list even though the gameID
doesn't change.

**Gating** — new authenticated endpoint `POST /api/game/:id/listing`:
- creator-only (403), private + not-started only (409)
- fresh subscription check via server-side `getUserMe` using the shared
`hasActiveSubscription()` helper (`active`/`trialing`); skipped in
`GameEnv.Dev` (same precedent as Turnstile) so it's testable locally
- one-lobby-per-creator (409): a SHA-256 hash of the creator's
persistentID rides worker↔master IPC (`PublicGameInfo.creatorID`); the
master dedupes as a race backstop. The hash — and host-only config
(whitelist, name reveals) — are **stripped from every client payload**
(broadcast + primed snapshot).

**Client** — subscriber-gated "List lobby publicly" toggle in the host
modal (server rejection reverts the toggle and shows a translated
message); "Open Lobbies" rows (map, mode, player count) in the Join
Lobby modal that reuse the existing private-join flow.

**Compat** — `PublicGames.games` is now a `partialRecord`, so newer
clients tolerate servers that don't send every bucket. Note:
already-open old clients will fail to parse broadcasts containing the
new `hosted` key until refreshed (closed Zod enum) — same class of break
as previous wire-schema changes.

## Testing

- `tests/server/HostedLobbyListing.test.ts` (15 tests): listed-lobby
filtering, flag not settable via config intent, master aggregation +
creator dedupe + no scheduling of hosted, creatorID stripping (broadcast
+ primed snapshot), `creatorHasListedLobby` (broadcast + local),
fingerprint refresh on config change
- `hasActiveSubscription` cases in `ApiSchemas.test.ts`; hosted
counts-delta patch in `LobbySocket.test.ts`
- Full suite green (1723 + 141 tests), tsc/eslint/prettier clean
- **E2E in the real app** (headless Chromium, two browser contexts):
host lists lobby → appears in second browser's Join Lobby list
(creatorID absent from payload) → join succeeds (2 players in lobby) →
same creator's second lobby rejected 409 with toggle revert → unlist
removes it from a fresh browser's list. Curl negatives: missing auth
400, bad token 401, non-creator 403, missing game 404, bad body 400.

## Known follow-ups

- Custom lobby name/description in the browser (needs the censor
pipeline) — rest of #4040
- A listed lobby whose host closes the tab stays advertised indefinitely
(an empty private lobby never leaves the Lobby phase) — pre-existing
lifecycle, now more visible; consider delisting on creator disconnect

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 20:25:56 -07:00
RickD004andGitHub 36aa38bcd0 Multiple terrain changes and fixes for maps (#4502)
## Description:

Multiple changes for maps:

Mississippi - Fix some landlocked lakes, near the main coast. Players
would sometimes accidentally put a port in the dead lakes

North America - Re-add the Azores and Cape verde (accidentally removed
when the map generator got updated to delete small islands)

Africa - Re-add Ascencion and Saint Helena Islands, same case as NA

Danish Straits - replace german empire flag , as agreed in the leads
channel

Giant World Map - Coast fixes, including adding Suez Canal which greatly
affected trade routes

Great Lakes - fix landlocked lake

Two Lakes - New Terrain combining both old and recent versions

World - Add strait of malacca

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

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

tri.star1011
2026-07-04 19:39:56 -07:00
eae2be6458 feat(store): confirm plutonium and caps purchases before charging (#4510)
## Summary

Fixes #4218

Currency purchases (Plutonium and Caps) fired immediately on click with
no confirmation. This adds a confirmation modal — reusing the existing
`confirm-dialog` Lit component — that gates every currency purchase
behind an explicit "Confirm".

There were two paths that could trigger a currency purchase, and both
are now gated:

- **The Plutonium / Caps price buttons** — `PurchaseButton` no longer
calls `onPurchaseHard`/`onPurchaseSoft` directly; it opens a
`confirm-dialog` ("Buy {item} for {amount} {currency}?", warning
variant) and only runs the purchase on confirm. Cancel / backdrop click
dismisses.
- **Whole-card click** — `CosmeticContainer` auto-fires the purchase
when there's exactly one payment option, which bypassed the button
entirely for currency-only items. That path now delegates to the
purchase button's new `requestCurrencyPurchase()` so it goes through the
same dialog.

The existing purchase flow (busy guard, loading overlay,
insufficient-currency dialog) is unchanged and runs after confirmation.
Dollar purchases are untouched (they go through Stripe checkout, which
is its own confirmation step).

New i18n keys: `store.confirm_purchase_title`,
`store.confirm_purchase_body` (en.json only, per Crowdin convention).

## Test plan

- [x] ESLint, `tsc --noEmit`, Prettier pass
- [ ] Manual check in staging: click a Plutonium or Caps price button →
dialog appears with the right currency name and amount; confirm
purchases, cancel doesn't
- [ ] Manual check: click the card body of a currency-only item → same
dialog (not an instant purchase)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:52:17 -07:00
be77ab4fc9 feat: structures cosmetic effect (hover-shown gradient/transition recolor) (#4492)
## Description:

Adds a new `structures` cosmetic effect type: an equippable effect that
recolors the owner's structure icons (City, Port, Factory, Defense Post,
SAM Launcher, Missile Silo) with gradient or transition color styles.
The effect is **shown while the owner's territory is hovered** —
structures otherwise keep their normal player colors, so the map stays
readable.

**Cosmetics / selection**
- `StructuresEffectAttributesSchema` (`CosmeticSchemas.ts`): its own
discriminated union (`gradient` / `transition`) — structurally identical
to the trail attributes today, but structures aren't trails, so it's a
separate schema free to diverge.
- Slot = the effectType itself: `effectTypeForSlot` is generalized to
map any non-nukeExplosion effect type to itself, so server privilege
checks (`Privilege.ts`), client selection, and persistence all work with
no per-type code.
- Effects tab, Default tile, and the store preview (shared color swatch)
come from `EFFECT_TYPES`; the only UI addition is the
`effects.type.structures` label in `en.json`.

**Rendering**
- The shared per-player effect palette grows from 2 to 3 blocks
(`EFFECT_PALETTE_BLOCKS`; structures = block 2, pinned by a
build-breaking guard). `syncPlayerEffects` resolves the `structures`
selection through the same `writeEffectEntry` used by trails.
- `StructurePass` binds the effect texture plus `uTime` and
`uHoverOwner` (fed from the existing `HoverHighlightController` →
`setHighlightOwner` path, now forwarded to the pass).
- `structure.frag.glsl` recolors the **fill only** — the border keeps
the player color for ownership legibility; alt view and construction
gray bypass the effect entirely.
- Style semantics:
- `gradient` — the palette spans each icon's diagonal once (a visible
gradient across the shape), sliding one full cycle every `colorSize · 4
· count / movementSpeed` seconds (the trail-equivalent pace; world-space
banding like the trail's would put a whole icon inside one band and read
as a flat color)
- `transition` — the whole icon is one color at a time, cross-fading at
`frequency` colors/s
- Glyph contrast: the inner icon's black/white decision is now a smooth
luminance fade (`smoothstep(0.25, 0.45)`) instead of a hard flip at
0.25, so animated fills cross-fade the glyph instead of snapping it
between black and white.

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:41:39 -07:00
Zixer1andGitHub 78ef7b56fd feat(doomsday-clock): battle-royale style zone gamemode (#4469)
Resolves Issue #4463

## Description:

An optional game mode that (almost) guarantees a finish instead of
letting late-game
stalemates drag on.
Originally called sudden death, renamed to Doomsday clock

Once enabled, every side (each player in FFA, each whole team in team
modes)
must hold a rising share of the map. A side below the bar is skulled;
after a
short warn its troops bleed to zero, forcing consolidation to a winner.

### How it works
- **Rising zone:** a grace period, then the required share ramps up
linearly to
each level with 30s pauses between (a battle-royale "zone"). Levels
track the
  ofstats FFA territory median (3/5/10/20/30%).
- **Four speed presets** (slow / normal / fast / very fast) change only
the pace:
  normal ends ~30 min, very fast ~15.
- **Troop decay:** a linear ramp as a % of max capacity, ~50s from
caught to zero
  (10s warn + ~50s ≈ 1 min total).
- **UI:** a HUD panel (live share vs target, wave/decay countdowns,
red/orange
cues) and an on-map skull above flagged players (blinks in danger,
steady while
  draining).

### Notes for review
- Off by default; no effect on existing games. However, as discussed we
can add it to the modifier pool for public games to see how popular the
gamemode is vs normal play.
- Sim is deterministic (integer-only, in `src/core`), covered by unit +
  integration tests.
- One-line addition to `GameServer.updateGameConfig` so the setting
survives the
  host → server → client round-trip.
- Status is packed into the existing name-pass data slot (`pd4.w`: 0/1/2
=
none/danger/draining); the skull is composited into the icon atlas at
load.

### Testing
`npm test`, `npm run lint`, `npx prettier --check .`, `npm run
build-prod` all pass.

### UI:
<img width="243" height="100" alt="Image"
src="https://github.com/user-attachments/assets/c4c9eeb0-4feb-437d-9aac-b2786a841b74"
/>

Dropdown between slow, normal, fast, very fast

Before zone:
<img width="302" height="175" alt="Image"
src="https://github.com/user-attachments/assets/7359a1ea-4951-446d-a23c-0711fe06cc5d"
/>

Zone started, player not affected the pannel also blinks orange for 10s:
<img width="297" height="175" alt="Image"
src="https://github.com/user-attachments/assets/fcc565a5-d5d0-47a7-97ea-d0ba9d9ad899"
/>

Player affected, grace period (Danger):
<img width="314" height="170" alt="Image"
src="https://github.com/user-attachments/assets/ff96d21e-96f3-4ef9-8190-48eecc7aac0f"
/>

Skull icon blinking over player (everyone sees it) - older screenshot,
the clipping has been fixed
<img width="462" height="145" alt="Image"
src="https://github.com/user-attachments/assets/53899211-33b1-40e1-83f2-77f2096f0cad"
/>

Player affected, grace period ended (Draining):
<img width="360" height="159" alt="Image"
src="https://github.com/user-attachments/assets/4b226d57-da4d-4866-ab5f-db48e4ed1ea2"
/>

Skull icon no longer blinking, everyone can see you are in a state of
decay, and troops are draining:
<img width="732" height="146" alt="image"
src="https://github.com/user-attachments/assets/cd10fedb-6e87-4dfc-9fbf-55d3945a7901"
/>


Skull is visible like alliances icon also on player tab
<img width="558" height="81" alt="Image"
src="https://github.com/user-attachments/assets/6acdbe91-bdd0-40c7-942b-3990d4dae87f"
/>

(just UI example, best way to see it is to hop on a solo game and play
against AI)

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

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

zixer._
2026-07-02 18:42:03 -07:00
bijxandGitHub ad760a0f3d feat: Achievement medal overview (#4487)
## Description:

In the spirit of achievement hunting, seeing how many medals a player
has achieved helps show overall progress. Overview only toggles on when
user clicks toggle achievements button. Works on mobile too.


https://github.com/user-attachments/assets/ea77075b-5e91-4e62-8ac9-52bcdf95cc64

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

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

bijx
2026-07-02 18:31:19 -07:00
crunchybbbandGitHub a7245518e2 Adds Map of China (#4468)
> **Before opening a PR:** discuss new features on
[Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small
improvements as
[issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose).
You must be assigned to an `approved` issue — unsolicited PRs will be
auto-closed.

**Add approved & assigned issue number here:**

Resolves #4419

## Description:

- Adds a China Map. This is themed after Qing China from the late 19th
and early 20th centuries, which is why the map also includes Mongolia
and Taiwan.
- There are 32 nations themed after qing provinces, warlords, factions,
European colonies, rebellions.
- Also adds several new flags: EastTurkestan, Manchukuo,
BritishHongKong, TaipingHeavenlyKingdom, etc.
- Includes additional nations for a total of 62 nations.
- Size: 1892x2080
- Land area: approx. 1.86M
- Adds Caspian Sea and China to the new category (which i forgot
earlier)


https://youtu.be/ew9Qizo67cA

<img width="2082" height="1895" alt="image"
src="https://github.com/user-attachments/assets/d34fbb9e-fa93-4afe-a89f-8f11c5f9529b"
/>


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

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

DISCORD_USERNAME crunchybbbbb
2026-07-02 15:13:26 -07:00
bijxandGitHub aadca4e0ab Update warship instructions for veterancy (#4489)
## Description:

Adds simple help instructions for new warship veterancy to help screen.
Just to be clear about what levelling does and how many levels can be
achieved.
<img width="1136" height="388" alt="image"
src="https://github.com/user-attachments/assets/c14425b4-ea23-4b62-89b3-881f8c16e1f3"
/>


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

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

bijx
2026-07-02 14:32:04 -07:00
6ff202afb5 feat: nuke-explosion cosmetic effects (per-bomb-type shockwave customization) (#4485)
## Description:

Adds a new `nukeExplosion` cosmetic effect type: when a bomb detonates,
every client renders the shockwave in the firing player's equipped
effect for that bomb type.

**Cosmetics / selection**
- New `nukeExplosion` effect schema (`CosmeticSchemas.ts`) with per-bomb
selection slots — a slot is the effectType for trails and the `nukeType`
for explosions (`atom` / `hydro` / `mirvWarhead`), so players can equip
a distinct explosion per bomb type.
- Slot resolution + validation is one shared helper
(`findEffectForSlot`) used by client selection, server privilege checks
(`Privilege.ts`), and the renderer; a compile-time guard keeps the
nukeType and effectType slot namespaces disjoint.
- Effects picker gains an Atom / Hydrogen / MIRV sub-tab bar when
browsing nuke explosions; selections persist per slot in UserSettings
and are validated/dropped like other cosmetics.

**Rendering**
- `WebGLFrameBuilder` resolves each dead nuke's owner cosmetic onto the
dead-unit event; `FxShockwavePass` renders an EMP-style procedural ring
(jagged crackling front, rotating lightning arcs, inner energy fill)
from per-instance attributes. SAM interceptions and players with no
cosmetic keep the classic white ring.
- Catalog attributes have literal units:
- `size` — final ring width (diameter) in world tiles at fade-out,
absolute — independent of the bomb's blast radius
- `speed` — tiles/s the width grows; duration = size / speed, clamped to
0.1–15 s
- `thickness` (required) — ring band thickness in tiles, constant while
the ring expands
- `colors` — palette of up to 4 colors, cycled at `transitionSpeed`
steps/s (0 = static, negative = reverse; same semantics as trail
transitions)
- The shockwave quad is sized radius + thickness so the absolute-width
band isn't clipped into a box while the ring is young.

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:21:01 -07:00
1d5a6ae246 Account Modal - Games Tab (#4473)
## Description:

Continuation of https://github.com/openfrontio/infra/pull/386, adds play
games sessions

<img width="971" height="771" alt="image"
src="https://github.com/user-attachments/assets/42c6bcbb-d690-4cd1-b859-3299a03f4350"
/>

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

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

w.o.n

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:12:37 -07:00
c9850c472b docs: remove Wicked Sick service-provider paragraph from privacy policy (#4472)
Removes the paragraph in Section 1 ("Who We Are") of the privacy policy
that named Wicked Sick Limited (company number 11117702) as a service
provider, and bumps the "Last Updated" date to 7/1/2026.

- Repo-wide search confirmed no other references to "Wicked Sick" /
"11117702" anywhere in the codebase.
- Section 1 reads correctly after removal (data-controller block flows
into the EEA/UK representative paragraph).

Replaces closed PR #4471.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 07:56:50 -07:00
2794ab1270 feat: nuke-trail cosmetic effect + tabbed effects picker (#4466)
## What

Adds a **`nukeTrail`** cosmetic effectType alongside
`transportShipTrail`, so nukes leave a trail colored by their own
gradient/transition effect — independent of the boat-trail effect (a
player can run both). Also reorganizes the effects picker and store into
per-effectType **tabs**.

## Rendering

Boat and nuke trails are stamped into **one** trail texture keyed only
by owner, so independent coloring needs a per-tile unit-class signal:

- **Trail texture** `R8UI` → `R16UI`: texel = `ownerID(bits 0-11) |
nukeBit(bit 12)`. `TrailManager` stamps the bit (and preserves it when
repainting on unit death); the `Uint8Array`→`Uint16Array` ripple +
`UNSIGNED_SHORT` uploads flow through `GpuResources`, `TrailPass`,
`Upload`, `MapRenderer`, `Renderer`, `FrameData`.
- **Effect texture** widened to two stacked blocks
(`TRAIL_EFFECT_BLOCKS`): rows 0–7 = transportShipTrail, rows 8–15 =
nukeTrail. `writeEffectEntry(…, rowBase)`; `syncPlayerEffects` resolves
both effectTypes.
- **Shader** masks the owner, derives `rowBase` from the nuke bit,
offsets every row, and reuses the gradient/transition decode.
- Bonus: the 12-bit owner mask lifts the old `R8UI` >255-player
truncation.

## Schema / server / UI

- Shared attributes schema renamed `TransportShipTrail…` →
**`TrailEffectAttributesSchema`** (it's no longer ship-specific);
`NukeTrailEffectSchema` added to `EffectSchema` +
`CosmeticsSchema.effects`. `EFFECT_TYPES = [transportShipTrail,
nukeTrail]`.
- Server `Privilege`, selection, and the picker grid all iterate
`EFFECT_TYPES`, so they handle the new type with **no per-type code**.
- **Tabs:** the selection modal uses one tab per effectType
(`BaseModal`'s native tabs); the **store's** EFFECTS panel gets an
internal sub-tab bar (its top-level PACKS/EFFECTS tabs can't nest). Tabs
are always present, so a type you own entirely still appears as an empty
tab (previously the boat-trail section vanished from the store when you
owned everything).

## Review

A 3-angle adversarial review (bit-packing, type-ripple, GLSL/data-flow)
**refuted** the correctness concerns — the R16UI format, masking, and
block layout agree across `TrailManager` / shader / builder. The minor
survivors (a preview that only resolved boat trails, stale comments)
were fixed.

## Testing

- `tsc --noEmit`, ESLint, Prettier, `build-prod` — all clean.
- Schema/`Privilege` tests updated for `nukeTrail` (96 tests pass).
- The GL trail + tab UI are visual — not yet verified in a running game.
- The catalog (`cosmetics.json`, closed-source API) must ship the
`effects.nukeTrail` block for the effect to appear in production.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:13:41 -07:00
RickD004andGitHub ae0d9f8d5e Adds "Levant" map (#4456)
## Description:

Adds map of the Levant / Levantine Sea. This map is a square map with 3
sides being land, with an inner sea, which itself has a large island
(Cyprus, slightly moved and resized for balancing purposes).

After v32 added a trading buff, island players have been going crazy,
with a map like this we could have crazy strong pirate players, which
will make for fun and interesting gameplay.

Nations are based on medieval states from the Crusades. Also adds more
additional nations for a total of 62, for gamemodes like HumanVSNations
and Solo. Also adds flags for the map



https://github.com/user-attachments/assets/286432bd-011b-4716-85c9-20811777ff65


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

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

tri.star1011
2026-06-30 11:49:21 -07:00
RickD004andGitHub cd5f8a6998 Adds map "Tierra del Fuego" (featuring impassable terrain) (#4437)
## Description:

Adds map of Tierra del Fuego, the southernmost tip of South America:
https://en.wikipedia.org/wiki/Tierra_del_Fuego

This map uses impassable terrain to stylize the borders of this map into
a triangular shape, this zone is known as "end of the world" so i
thought it would be visually fitting if the borders converged at the
bottom of the map.

800k land tiles similar to Caucasus.

This map has a lot of islands, incluiding the Falklands for people who
enjoy island-plays, and also a chokepoint (the strait of Magallanes) for
piracy.

Also adds flags of Chilean and Argentinian provinces for the map and the
menu.

<img width="818" height="544" alt="image"
src="https://github.com/user-attachments/assets/59812868-7514-4e03-86a2-072cf4013aeb"
/>


https://github.com/user-attachments/assets/acc15020-bd7a-49e0-a504-8575a1e74f75

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

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

tri.star1011
2026-06-30 09:42:55 -07:00
TKTK123456andGitHub 0d2179f5f3 Input handler.ts rework (#4225)
> **Before opening a PR:** discuss new features on
[Discord](https://discord.gg/K9zernJB5z) first, and file bugs or small
improvements as
[issues](https://github.com/openfrontio/OpenFrontIO/issues/new/choose).
You must be assigned to an `approved` issue — unsolicited PRs will be
auto-closed.

**Add approved & assigned issue number here:**

Resolves #4193 

## Description:

Use activeKeys set in places where it is checking if a key is being
pressed in a different way, and it makes more sense to use the
activeKeys set. Make the overall code of the InputHandler.ts file more
consistent and to make it easier to add new keybinds in the future.

<img width="1920" height="1080" alt="Screenshot from 2026-06-13
20-49-56"
src="https://github.com/user-attachments/assets/94f6f81c-7278-4bca-845c-2442b6caea39"
/>


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

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

tktk1234567
2026-06-30 14:17:25 +00:00
evanpelleandClaude Opus 4.8 f4b47ce06c feat: add search bar to effects picker modal
Mirror the flag/pattern modals with a search input in the EffectsModal
header. Filters the effects grid by name — matching either the raw effect
id or its displayed label — and hides the Default tile while searching,
the same way FlagInputModal hides its no-flag tile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:38:37 -07:00
6a884eba1b store popup (#4435)
## Description:

change the generic popup:
<img width="1095" height="540" alt="image"
src="https://github.com/user-attachments/assets/94d2c120-5ec5-4838-b8b4-09d43b4e83f8"
/>


into a popup i added for clan system:
<img width="1108" height="774" alt="image"
src="https://github.com/user-attachments/assets/d7de1666-7667-4422-a1bd-03b90b4ff8ab"
/>

caps doesn't have a "buy" button:
<img width="1141" height="803" alt="image"
src="https://github.com/user-attachments/assets/d26dd397-1f14-4963-8ac8-afa5f32ed8ec"
/>

also works for win modal:
<img width="1023" height="766" alt="image"
src="https://github.com/user-attachments/assets/83f7bc87-0ecc-4470-b84d-c5783560d6a3"
/>

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

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

w.o.n

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:24:09 -07:00
bd9ef9a317 feat: effects cosmetic category (transport-ship trail) + UI (#4418)
## What

Adds a new **`effects`** cosmetic category alongside `skins`/`flags`.
Each effect is discriminated by **`effectType`** (only
`transportShipTrail` today), whose visual config lives in
**`attributes`** (`solid` / `rainbow` / `pulse` / `gradient`). Schema
matches the production cosmetics.json shape exactly (incl. the `url`
field).

**This PR is UI + taxonomy only — the in-game WebGL trail rendering is
intentionally deferred.**

## UI

- **Store** gains an **"Effects"** tab.
- **Home page** gains an **"Effects"** button opening a picker modal.
- Both render effects **grouped by `effectType` with a sub-header per
type**, via a shared `<effects-grid>` Lit element (`mode="select"` for
the picker, `mode="purchase"` for the store). The picker shows owned
effects + a Default tile and persists per-type; the store shows
purchasable effects.

## Data flow

- Ownership via `effect:*` / `effect:<name>` flares (reuses
`cosmeticRelationship`).
- Selection is a per-`effectType` map persisted in UserSettings
(`settings.effects`).
- Server validates in `isEffectAllowed`, wired into `isAllowed`.
- `getPlayerCosmeticsRefs` / `getPlayerCosmetics` resolve effects the
same way as skins/flags (kept-on-fetch-failure, server is authority).

## Tests

- `tsc --noEmit`, ESLint, Prettier clean; full suite green.
- New: `CosmeticSchemas` parse tests (incl. parsing the **real**
`read_transport_trail` entry), `UserSettings` per-type selection, and
`Privilege` effect validation.

## Notes / follow-ups

- The effect's display label shows **"Boat Trail"** for the
`transportShipTrail` type (friendlier than the id).
- Closed-source API gap: `/shop/purchase` (`purchaseWithCurrency`) needs
to learn `"effect"` for **currency** purchase of effects; the
**dollar/product** purchase path already works. Client types were
widened accordingly.
- In-game wake rendering can be ported from #4416.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:13:48 -07:00
bijxandGitHub 06d505ebc9 feat: Expand maps button (#4431)
## Description:

QoL fix w.r.t. "All" maps category. Maps used to all be shown in a big
list, now the category sections need to be expanded one by one.
Categories are super clean and useful, but to visually see and pick maps
when you're not certain which one you want to play becomes challenging
(to click and expand each category manually). This just adds a simple
"Expand All" and "Collapse All" button to the all category list on Solo
and Private Match screens.


https://github.com/user-attachments/assets/e5d7a754-a6b6-461c-b039-7b6a8d3bee46


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

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

bijx
2026-06-28 19:19:22 -07:00
RyanandGitHub 966dcf47a5 removes the "subscribe" and "purchase" text (#4436)
## Description:

removes the "subscribe" and "purchase" text:

<img width="988" height="496" alt="image"
src="https://github.com/user-attachments/assets/9566b450-0943-4684-8321-8024422bbd96"
/>

<img width="993" height="505" alt="image"
src="https://github.com/user-attachments/assets/f26bfd99-661d-48e3-beb3-e3e5d212e6f1"
/>

<img width="1002" height="511" alt="image"
src="https://github.com/user-attachments/assets/c8f2aadf-15d1-4e29-8142-f684c6e492f0"
/>

this is what it looks like if you're subbed to something now:
<img width="997" height="491" alt="image"
src="https://github.com/user-attachments/assets/5f011213-7ced-4a64-860e-45a6b0a7418f"
/>


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

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

w.o.n
2026-06-28 18:41:32 -07:00