Commit Graph

4183 Commits

Author SHA1 Message Date
MushroomLamp 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
Evan 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
Evan 8405c8e896 feat(store): show plutonium and caps balances in store header (#4570)
## Summary
- Show the player's plutonium (hard) and caps (soft) balances on the
right side of the store modal's title bar, next to the existing
not-logged-in warning
- Reuses the existing `currency-display` component from the account
modal
- Renders only when the user is logged in and has currency data
(`player.currency` is optional in the API schema); balances stay current
via the `userMeResponse` document event the store already listens to

## Test plan
- [ ] Open the store while logged in with currency balances — plutonium
and caps appear to the right of the title
- [ ] Open the store while logged out — only the not-logged-in warning
shows, no currency display

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:14:26 -07:00
RickD004 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
crunchybbb 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
JB940 f1fc5434be fix classes for controlpanel unit display and control panel/playerinfo gold display (#4562)
> **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 #4411

## Description:

This PR removes three extra classes in the UI causing issues but no
additional functionality. Gold UI elements were artificially not scaled.
I have confirmed on mobile devices and different layouts, the containers
behave exactly the same way, unless people have an accessibility feature
like scaled text size OR the text grows too large, in which case they
now properly fit into the container as the fix suggests.
Before:
<img width="653" height="116" alt="image"
src="https://github.com/user-attachments/assets/70c81cdd-8bdc-40cc-977e-b045078eafc0"
/>
After:
<img width="636" height="119" alt="image"
src="https://github.com/user-attachments/assets/926fcb94-7944-46f9-a58a-adc7984278b1"
/>

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

JB940
2026-07-10 12:38:12 -07:00
Evan e48a424b32 Remove in-game CrazyGames banner ad (#4567)
## Summary

- Remove the bottom-left 300×250 CrazyGames banner ad shown during
gameplay (created when the spawn phase ends in `InGamePromo`), including
its cleanup path in `hideAd()`
- Remove the now-unused `createBottomLeftAd()` / `clearBottomLeftAd()`
wrappers and the `banner` portion of the CrazyGames SDK type declaration

On CrazyGames, `window.adsEnabled` is already forced to `false`
(Main.ts), so the Playwire in-game path can't activate there — with this
change the game screen shows no ads at all on the CrazyGames platform.
Midgame video interstitials (singleplayer start, game exit) are
unchanged.

## Test plan

- [x] `npx tsc --noEmit` passes
- [x] ESLint passes on touched files
- [ ] On CrazyGames: no banner appears bottom-left after spawn phase
ends

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:28:23 -07:00
Evan 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
TKTK123456 6a6c142388 Fixed pirating not being disabled when a warship's owner has no port in that body of water (#4458)
> **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 #4291 

## Description:

Fixes pirating not being disabled when a warship's owner has no port in
that body of water

## 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-07-10 16:31:21 +00:00
iamlewis 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
Ryan 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
SpeakIsntThere 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
Ryan 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
Evan 274b516ea3 feat(crazygames): backend login + surface the signed-in user (#4542)
Client-side CrazyGames login: exchange the SDK user token for our
session, then surface that identity in the UI. Implements the client
side of the [CrazyGames login handoff
guide](https://docs.crazygames.com/sdk/user/).

## Part 1 — Backend login (token exchange)

On CrazyGames we exchange the SDK's user token for our own session via
`POST /auth/crazygames`, instead of the cookie-based `/auth/refresh`
(the refresh cookie is `SameSite=Lax` and unusable from the CrazyGames
iframe).

- **`CrazyGamesSDK.ts`** — `getUserToken()` wrapper (awaits `ready()`,
gates on `isUserAccountAvailable`, returns `null` on throw / no
signed-in user).
- **`Auth.ts`** — `doRefreshJwt()` routes to `doCrazyGamesLogin()` when
on CrazyGames with a signed-in account; a `null` token (guest / no
account) falls through to the existing `/auth/refresh` flow.
`reauthAfterCrazyGamesChange()` drops the cached session on a
mid-session sign-in.
- **`Main.ts`** — `addAuthListener` re-runs auth + `getUserMe()` when
the player signs into CrazyGames mid-session.

Everything funnels through the existing `userAuth()` → `refreshJwt()`
path, so **startup login and the 15-min re-exchange on expiry come for
free** — no new expiry/polling code.

## Part 2 — Surface the signed-in user

- **Account button** shows the CrazyGames avatar + username when signed
in (clicking opens the account modal), or a **"Sign in"** that opens
CrazyGames' own `showAuthPrompt()` when a guest. Wired across every
entry point: the desktop nav pill, the mobile hamburger item (un-hidden
on CrazyGames by dropping `.no-crazygames`), and — since CrazyGames
renders below the `lg` breakpoint where the desktop nav is hidden — a
new button in the **homepage top bar's** right slot.
- **AccountModal** treats the CrazyGames user as logged-in: "Connected
as" avatar + username, currency/subscription, and stats/games/friends.
**No Discord/Google/email login+link buttons and no logout** (CrazyGames
owns the account). A guest who reaches the modal gets a CrazyGames
sign-in button, never Discord/Google.
- **`CrazyGamesSDK.ts`** — `getUserProfile()` + `showAuthPrompt()`
wrappers; `profilePictureUrl` added to the user type.

Identity comes from the SDK (`getUser()`), not `/users/@me`, which
doesn't surface CrazyGames identity yet.

## Behavior

- **Signed-in CrazyGames account** → real backend session; avatar +
username in the top bar; CrazyGames-only account modal.
- **CrazyGames guest / not signed in** → silent fallback to guest; "Sign
in" button opens `showAuthPrompt()`; auto-logged-in the moment they sign
in (via `addAuthListener`).
- **Not on CrazyGames** → completely unchanged.

## Testing

- `npx tsc --noEmit` — clean
- `npx eslint` on changed files — clean
- No new i18n keys (reuses `main.sign_in`, `account_modal.*`).
- No unit tests: this repo tests core sim only, and CrazyGames only
initializes inside a crazygames.com iframe, so the CG paths can't run
locally (the localhost SDK mock returns unsigned tokens the backend
rejects). **Needs a manual pass on the CrazyGames game page (gameId
`64178`)** covering: signed-in avatar/username + account modal, guest
"Sign in" → prompt, and mid-session sign-in.

## Assumptions to confirm (backend)

1. The `/auth/crazygames` JWT carries the same `iss` (`getApiBase()`)
and `aud` (`getAudience()`) as normal openfront.io tokens and satisfies
`TokenPayloadSchema` (incl. base64url `sub`) — otherwise `userAuth()`
will `logOut()`.
2. CrazyGames iframes our own origin (so `getApiBase()` →
`https://api.openfront.io`), consistent with the existing integration.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 08:02:45 -07:00
SpeakIsntThere 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
Zixer1 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
Zixer1 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
Evan 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
Patrick Plays Badly 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
blon 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
NotRocketfish 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
Zixer1 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
RickD004 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
blon 0859efc06a fix(client): prevent Google CCPA button from shifting layout (#4519)
## Description:

When Google Funding Choices CMP dynamically injects the CCPA "Do Not
Sell or Share My Personal Information" button (`.fc-dns-link`) directly
into the document `<body>`, it becomes a flex child because the body
uses a `flex flex-row` layout. This pushes the main page container to
the left.
The PR fixes that.

## Please complete the following:

- [x] I have added screenshots for all UI updates *(Tick this if you
took a screenshot of the injected button)*
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] I have added relevant tests to the test directory 

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

blontd6
<img width="1470" height="839" alt="Screenshot 2026-07-06 at 7 58 47 PM"
src="https://github.com/user-attachments/assets/b8aa20e4-c3af-4cf5-a382-3c4c732bff49"
/>
2026-07-06 18:31:13 -07:00
Evan 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
Evan 46b9fe88bb fix: survive host modal close during listed-lobby auto-start (#4514)
Follow-up to the listed-lobby auto-start feature (`2a82b01e3`, on main).

## Bug

When a listed lobby auto-started (and in some paths on manual start),
the game began server-side but a player got ejected back to the menu
before it loaded.

Root cause: `BaseModal.close()` navigates via `showPage()`, which
**force-closes whichever page-modal is currently visible**. During the
game-start transition that is the *other* lobby modal — closing the join
modal first cascade-closed the host's modal with `leaveLobbyOnClose`
still armed (host disconnected → host-left teardown killed the game);
reversing the order just moved the victim to the joiner. No close order
fixes both sides.

## Fix

- **Disarm both lobby modals before closing either** in the prestart
transition (`disarmLeaveOnClose()` on `HostLobbyModal` and
`JoinLobbyModal`), so no cascade order can disconnect anyone mid
game-start.
- Both modals re-arm `leaveLobbyOnClose` in `onOpen` instead of
`onClose`'s state reset, so once disarmed no later cascade close can
re-arm it until the modal is reopened.
- `HostLobbyModal.closeWithoutLeaving()` (pattern `JoinLobbyModal`
already used), called explicitly instead of the generic modal-close
loop.
- **Server hardening:** `handleClientDisconnect` bails on `hasStarted()`
(which includes prestart) instead of raw `_hasStarted`, so host socket
churn during the lobby → game transition can never tear down a starting
game regardless of client behavior. Regression test included.

## Verification

Headless end-to-end (two browser contexts, host + joiner, `leave-lobby`
stack-trace tripwire armed on both pages):

- **Manual start:** host and joiner both receive prestart/start and
enter the game; zero `leave-lobby` dispatches.
- **Auto-start:** same, with nobody interacting after listing.

Also confirmed manually in a real browser. The auto-start deadline was
temporarily 30s during testing and is restored to 5 minutes in the final
commit. Full suite green (1843 + 161), tsc/eslint/prettier clean.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 10:55:16 -07:00
evanpelle 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
Evan 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
Evan 22c873cf55 perf(client): tick-dispatch timing harness + main-thread tick optimizations (late-game p95 −65%) (#4512)
## Problem

Every 100 ms the main thread's worker `onmessage` callback processes a
full game tick (`gameView.update` → `webglBuilder.update` →
`renderer.tick`). At 60 fps this competes with the 16.7 ms frame budget,
and on the Giant World Map it takes several ms — frame drops on low-end
hardware.

## Harness (`npm run perf:client-tick`)

Headless-Chromium harness that times every worker→main `game_update`
dispatch on the main thread, with structured-clone deserialization
measured separately from the handler body (via a
`Worker.prototype.addEventListener` wrapper installed as a page init
script — no product-code changes). It reports windowed distributions,
captures `.cpuprofile` files at chosen ticks, writes raw samples and an
end-of-run screenshot. `AnalyzeCpuProfile.ts` breaks a profile down by
inclusive time under the dispatch subtree.

Init scripts are passed as **strings**: tsx compiles function-form init
scripts with esbuild `keepNames`, whose injected `__name` helper doesn't
exist in-page and silently kills the game worker setup.

## Baseline (Giant World Map, 400 bots, headless)

Dispatch handler ms — cost **grows with game progression**:

| window | mean | p50 | p95 | max |
|---|---|---|---|---|
| tick 506 | 2.22 | 2.20 | 3.40 | 5.00 |
| tick 1506 | 2.60 | 2.00 | 7.00 | 10.40 |
| tick 2000 | 2.67 | 1.90 | **8.70** | **12.70** |

Deserialization is negligible (0.12 ms mean). CPU profiles attributed
the growing tail to the leaderboard's once-per-second refresh: its
Max-troops column calls `config().maxTroops(p)` for **all ~508
players**, and `PlayerView.units()` scanned **every unit in the game**
per call — O(players × units), growing as units accumulate.

## Round 1 — algorithmic fixes

- **GameView**: new `unitsOwnedBy(smallID)` — an active-units-by-owner
index built lazily at most once per tick. `PlayerView.units()` reads its
own units from it: O(own units) instead of O(all units). Also speeds up
unit display, player panel, and buildables queries.
- **NamePass.updateNames**: reads player state directly from the
caller's map by smallID instead of rebuilding three lookup maps per
tick; skips the slot-assignment sweep once every player has a slot.

After (same map, same spawn tile):

| window | mean | p50 | p95 | max |
|---|---|---|---|---|
| tick 506 | 2.12 | 2.00 | 3.10 | 5.20 |
| tick 1506 | 1.86 | 1.80 | 2.90 | 4.30 |
| tick 2000 | 1.74 | 1.60 | **2.40** | **4.70** |

Late-game p95 −65% (8.7 → 2.4 ms), worst dispatch −63% (12.7 → 4.7 ms),
and per-dispatch cost no longer grows with game progression. The
leaderboard disappeared from the dispatch profile entirely.

## Round 2 — allocation churn + time slicing

Aimed at GC pauses and low-end CPUs; measures flat vs round 1 on a fast
machine, as expected:

- **`FrameData.changedTiles`** is now the plain tile-ref array GameView
already builds instead of a per-tile `{ref, state}` object copy — heavy
battle ticks allocated tens of thousands of objects per tick for a
`state` field that was always 0. `TilePair` removed; `TerritoryPass`
buckets refs synchronously, so the live reference is safe.
- **`UnitView.lastPos`** is only re-sliced when a move actually appended
a position — the unconditional `slice(-1)` allocated an identical
1-element array per unit per tick, including for structures that never
move.
- **`NamePass.updateNames`** refreshes slots round-robin, a quarter per
tick — the full per-player diff pass spreads over ~400 ms, under the
existing 500 ms troop-text cadence; positions lerp continuously. Unnamed
slots and snap passes (seeks) are always processed so nothing pops in
late. Dispatch share: 17% → 13%.

Not sliced on purpose: tile ingest and frame upload need a consistent
per-tick snapshot (stale `GameMap` reads would leak into hover queries,
minimap, attack targeting) — a correctness risk not worth ~1 ms while
the worst dispatch already fits in a quarter of the frame budget.

## Verification

- `npx tsc --noEmit`, eslint clean; full suite green (1929 tests)
- 6 new GameView tests cover the owner index (grouping, inactive
exclusion, ownership capture, death, type filtering, copy semantics);
changedTiles tests updated to the ref-array contract
- Headless end-of-run screenshots verified after each round: leaderboard
Max-troops values, map names + troop counts + flags all render correctly
(including with name slicing active)

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 13:46:02 -07:00
Evan 571f58440d perf(client): main-thread memory harness + drop three map-sized render buffers (-23%) (#4511)
## Summary

Follow-up to #4507, moving the memory-footprint campaign to the **main
thread** (client). Two parts: a headless browser measurement harness,
and a first optimization round that cuts the main-thread live heap on
Giant World Map by **23%** (166 → 128 MB at tick 2000).

## Part 1 — `npm run perf:client-mem`: headless main-thread memory
harness

Drives a real singleplayer game in headless Chromium and measures the
**page's isolate only** (the core sim worker is a separate CDP target):

- Starts its own vite dev server on a private port (default 9017) so it
always measures the current checkout.
- Double-forced-GC checkpoints every `--window` ticks: JS heap,
ArrayBuffer backing-store bytes (`Runtime.getHeapUsage`), DOM nodes,
listeners, ticks/s.
- `--snapshot-at <ticks>` writes V8 heap snapshots, analyzable with the
retainer/summary tools from #4507.
- Spoofs the unmasked WebGL renderer string via an init script so the
software-GL gate (#4324) admits SwiftShader — no game code touched;
rendering still runs software (hence the rAF throttle).
- End-of-run screenshot as a rendering sanity check.

Baseline (Giant World Map, 400 bots, 12,000 ticks): ~176 MB live, of
which ~116 MB is **static per-tile buffers** allocated up front for the
8M-tile map — flat during play, no leaks.

## Part 2 — drop three map-sized render-layer buffer copies

| Buffer | Before | After |
|---|---|---|
| `TrailPass.cpuTrailState` | 15.3 MB copy | **deleted** — dead code;
every upload entry point sets the live reference to TrailManager's array
|
| `RailroadPass.cpuRailroadState` | 15.3 MB across 2 arrays | references
`RailroadCache.railroadState` (stable identity, mutated in place) |
| `RailroadPass.cpuGhostRailState` | ↑ | sparse `Map<ref, value>`;
preview diffs applied as per-texel `texSubImage2D` writes (path-sized
work instead of a full 8 MB texture upload per build-preview mouse move)
|
| `TerrainPass` + `MapRenderer` terrain bytes | 7.6 MB (one buffer, two
retainers) | `terrainSource()` provider — re-bakes (theme change,
context restore) regenerate from the live game map, which already
reflects water-nuke conversions |

Tick-2000 snapshot comparison (giant world, 400 bots): **166.4 → 128.4
MB**.

## Verification

- `tsc --noEmit`, eslint, full test suite (1924 tests) pass.
- 2000-tick headless giant-world game after the change: no GL
pageerrors, end-of-run screenshot renders
terrain/territory/borders/names correctly, sim speed unchanged (~5
ticks/s headless).
- Ghost-rail ops flush before the zoom-fade early-return, so the op
queue can't grow while previewing at low zoom.
- WebGL context restore recreates all passes fresh and the owner
re-uploads state (existing `onContextRestored` path), consistent with
the new reference-based buffers.

Note: heap snapshots in `tests/perf/output/` are gitignored; the numbers
above are from runs recorded in the PR discussion.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 20:18:52 -07:00
RickD004 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
Evan 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
Evan 7fa81c6bb9 perf: reduce core live-memory footprint by 45% on large maps (#4507)
## Summary

Reduces the simulation's steady-state memory footprint. On Giant World
Map at 20 game-minutes (12 000 ticks, 400 bots, seed `perf-default`),
live memory after a full GC drops **293 MB → 161 MB (−45%)**; unforced
peak heap drops **326 MB → 165 MB**. The simulation also runs ~10%
faster (85 → 94 ticks/s). The final game-state hash is **bit-identical**
(`57830793797434300`) — no behavior change.

## Measurement (first commit)

The full-game perf harness gains a footprint mode:

- `--footprint` — forces a full GC at every `--window` boundary and
records the live heap / ArrayBuffer / RSS curve across the game
(requires `NODE_OPTIONS=--expose-gc`).
- `--snapshot-at 0,2000,12000` — writes V8 `.heapsnapshot` files at
chosen ticks.
- `HeapSnapshotRetainers.ts` — attributes every heap node to its nearest
meaningfully-named retainer (e.g. `PlayerImpl._tiles`), plus prints
retainer chains for all nodes ≥128 KB. `HeapSnapshotSummary.ts` is a
streaming fallback for snapshots too large to `JSON.parse`.

Baseline attribution at tick 12 000: player `_tiles`/`_borderTiles` Sets
**83 MB**, GameMap `refToX`/`refToY` lookup tables **38 MB**, two
duplicate 30.5 MB visited-scratch arrays, trade-ship stepper paths **15
MB**, a construction-only flood-fill queue **9.5 MB**.

## Optimizations

**Map-sized buffers (second commit):**
- `GameMap.x()/y()` compute `ref % width` / `(ref / width) | 0` instead
of reading two per-tile Uint16 tables (−38 MB). The arithmetic is
cheaper than the tables' random-access cache misses — this is where the
speedup comes from.
- `PlayerExecution` and `SpatialQuery` each kept their own per-game
generation-stamped visited `Uint32Array`; both now share one via
`TileTraversalScratch` (−30 MB).
- `PathFinderStepper` stores numeric paths as `Uint32Array` (half the
bytes; steppers hold their full path for a unit's whole journey).
- `ConnectedComponents` frees its flood-fill queue after `initialize()`.

**Player tile sets (third commit):**
- New `TileSet`: insertion-ordered set of tile refs backed by a dense
`Uint32Array` plus an open-addressing hash index — ~12 bytes/element vs
~34 for a native `Set<number>`. Deletes tombstone; compaction is
deferred while iteration is in progress so positions never shift under
an iterator.
- Iteration semantics match `Set` exactly (insertion order, entries
added mid-iteration visited, deleted ones skipped, delete+re-add moves
to end) — the simulation relies on this order for determinism, and the
unchanged hash confirms it.
- `Player.borderTiles()` now returns `ReadonlyTileSet` (a native `Set`
still satisfies it structurally); `GameRunner.playerBorderTiles` copies
into a real `Set` since that result crosses the worker boundary via
structured clone.

## Footprint curve (giant world map, live MB after forced GC)

| checkpoint | before | after |
|---|---|---|
| spawn end | 20 + 100 buf | 20 + 55 buf |
| tick 6301 | 119 + 161 buf | 29 + 127 buf |
| tick 12301 | 130 + 161 buf | 32 + 129 buf |

## Validation

- Final hash `57830793797434300` identical across baseline / round 1 /
round 2 runs (12 000 ticks).
- Full suite passes (1798 + 126 tests), including new `TileSet` tests:
order semantics, mutation-during-iteration parity with `Set`, tombstone
compaction, and a 20 000-op randomized differential test against native
`Set`.
- Runs recorded in
`tests/perf/output/footprint-{baseline,round1,round2}-giant.txt`.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:25:29 -07:00
evanpelle b0f85c5739 fix: keep Playwire bottom rail below the in-game HUD so it can't cover the control panel 2026-07-03 15:36:20 -07:00
Zixer1 66063d6178 feat(doomsday-clock): decay warships alongside troops for doomed sides (#4499)
## Description: 

Follow-up to #4469.

The Doomsday Clock drains a doomed side's troops but leaves its navy
untouched, so a coastal or island turtle can sit below the bar
indefinitely on warship defense, exactly the stall the clock is meant to
break.

This decays the warships of a flagged (sub-threshold, non-leader) side
on the same ramp as its troops:

- Each warship loses a percentage of its (veterancy-adjusted) max health
per second, reusing `doomsdayClockDrain`, so the fleet and the army
bleed in lockstep and reach zero together (~55s from full at the default
rate).
- Destruction passes **no attacker**, so it routes through
`UnitImpl.delete` as an environmental loss: no kill credit, no
boat-destroy stats, no veterancy granted. Scoring integrity is
preserved.
- Healing is suppressed for a flagged owner
(`WarshipExecution.healWarship` early-returns), so the decay actually
sinks the fleet instead of being out-healed at a port. Inert when the
mode is off, since the mark is never set.
- The leader's fleet is spared, same as its troops.

No new config: warships reuse the existing drain curve. No HUD change,
since warships count as part of the side's forces alongside troops.

Tested: 4 new unit tests (same-ramp decay, no-kill-credit destruction,
leader spared, warn-window grace), the full `DoomsdayClockExecution` and
`Warship` suites, the whole test suite (1784 passing), `build-prod`, and
a headless full-game sim run (resolves cleanly with the decay live,
deterministic).
2026-07-03 15:07:28 -07:00
Zixer1 2a1381b41e feat(doomsday-clock): show the zone readout to spectators and replay viewers (#4497)
## Description:
Follow-up to #4469.

The Doomsday Clock panel hides itself unless the viewer has a living
player, so spectators, replay viewers, and eliminated or not-yet-spawned
players see nothing, even though the clock is the most useful thing to
watch in those cases.

This shows the panel whenever the mode is on and there's no winner yet:

- **Live player:** unchanged. Full personal readout (your-share line,
Stable/Unstable/Collapsing status, danger pulse).
- **Spectator / replay / eliminated / not-spawned:** zone-only readout.
The rising threshold bar and the wave countdown ("Rising to X%", "Final
X%", "Next wave X% in mm:ss"), with no personal line and no false danger
pulse.

One gate drives it: `live = me && me.isAlive()`. The zone math was
already player-independent, so nothing new there.

Tested: `tsc --noEmit`, ESLint, Prettier, the `DoomsdayClockExecution`
suite (28/28), and `build-prod`, all clean.
2026-07-03 15:00:32 -07:00
Evan 20c81ca5f6 perf: cut core-sim GC churn another 36% (75% cumulative) (#4498)
## Summary

Round 3 of GC-churn reduction (follow-up to #4494 and #4496). All
changes are **behavior-preserving** — final game-state hash unchanged on
three seeded runs.

### Changes

| Site | Change | Churn target |
|---|---|---|
| `MiniMapTransformer` | Path upscaling works in pure numeric
coordinates and emits main-map `TileRef`s directly, replacing three
intermediate `Cell`-object arrays per path (cell path → scaled path →
smoothed path → final map). Identical arithmetic, so identical rounding
and identical tiles. | ~7.1 GB |
| `ShoreCoercingTransformer` | Reused neighbor buffers instead of
`neighbors()` arrays; no per-call `{water, original}` objects.
Tie-breaking preserved (helpers share the unified N,S,W,E order since
#4495). | ~1.5 GB |
| `diffPlayerUpdate` | Allocation-free all-equal fast path. Runs per
player per tick and usually returns `null` (gold/troops/tiles travel via
packed arrays), but previously allocated the diff object + a closure
first. Field list matches the diff exactly. | ~2.6 GB |
| Large-`Set` iteration | `for..of` over a `Set` allocates an
iterator-result object per element — significant on 100k-tile border
sets. `calculateClusters`, `calculateBoundingBox` (indexed fast path for
arrays too) and `getAttackFrontTiles` (also dropped its `neighbors()`
arrays) now use `Set.forEach`. | ~3.6 GB |

### Results (Giant World Map, 400 bots, 12,000 ticks, seed
`perf-default`)

| Metric | Before | After | vs. original (pre-#4494) |
|---|---|---|---|
| Sampled allocations (incl. collected) | 37.8 GB | **24.1 GB (−36%)** |
97.7 GB (**−75%**) |
| Ticks/sec | 82 | **88** | 66 (+33%) |
| Mean / p99 tick | 12.2 / 36.0 ms | 11.3 / 34.8 ms | 15.2 / 49.9 ms |
| Peak heap | 762 MB | 529 MB | 758 MB |

## Determinism

Final hash unchanged on all three reference runs:
- Giant World Map 12,000 ticks: `57830793797434300` ✓
- Giant World Map 2,000 ticks: `55125379638382860` ✓
- World 1,800 ticks: `32337437717390864` ✓

## Test plan

- [x] Full suite green (1,906 tests; the `getAttackFrontTiles` test stub
gained a `neighbors4` implementation to match the real interface)
- [x] Hash equality on 3 seeded headless runs (2 maps)
- [x] Before/after 20-min GC benchmarks

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:44:21 -07:00
Evan 9e9c608053 perf: cut core-sim GC churn another 36% (61% cumulative) (#4496)
## Summary

Round 2 of GC-churn reduction, attacking the next tier of allocation
sources found by the profiling harness from #4494. All changes are
**behavior-preserving** — the simulation is bit-identical (final hash
unchanged on three seeded runs).

### Changes

| Site | Change | Churn target |
|---|---|---|
| `Player.units()` / `Game.units()` | Rest parameter → fixed-arity +
array overloads (`units()`, `units(types[])`, `units(t1, t2?, t3?)`).
The rest array was allocated on **every call** of one of the hottest
functions in the sim. Spread call sites (`units(...Structures.types)`)
now pass the array directly. `GameImpl.units()` builds one flat array
instead of `Array.from().flatMap()` per-player intermediates. | ~18 GB |
| `PlayerExecution` cluster flood fill | Results are plain `TileRef[]`
in mark order instead of `Set<TileRef>` — the generation-stamped visited
array already deduplicates, and consumers only iterate/measure. DFS
stack reused across fills. | ~3.7 GB |
| `SpatialQuery.bfsNearest` | Fused generation-stamped BFS with per-game
scratch buffers (`WeakMap`-keyed, same pattern as `PlayerExecution`)
instead of materializing a `Set` of the entire search area per query.
Identical traversal and tie-breaking. | ~2.2 GB |
| `NationWarshipBehavior` ship tracking | Single-pass loops instead of
`filter().forEach()`; dropped defensive `Array.from(set)` copies
(deleting the current entry while iterating a `Set` is well-defined). |
~1.4 GB |

### Results (Giant World Map, 400 bots, 12,000 ticks ≈ 20 game-min, seed
`perf-default`)

| Metric | Before | After | vs. pre-#4494 |
|---|---|---|---|
| Sampled allocations (incl. collected) | 59.2 GB | **37.8 GB (−36%)** |
97.7 GB (**−61%**) |
| GC count / total pause | 1,076 / 1,830 ms | 772 / 1,442 ms | 1,682 /
3,313 ms |
| Ticks/sec | 73 | **82** | 66 (+24%) |
| Mean / p99 tick | 13.6 / 39.2 ms | 12.2 / 36.0 ms | 15.2 / 49.9 ms |

`units()` no longer appears in the top-30 allocator list at all. The
remaining leaders (possible round 3): the minimap pathfinding `Cell`
pipeline (~8.5 GB), `diffPlayerUpdate`/`toFullUpdate` per-tick
serialization (~4.6 GB), and iterator allocations (~3.3 GB).

## Determinism

Final game-state hash unchanged on all three reference runs:
- Giant World Map 12,000 ticks: `57830793797434300` ✓
- Giant World Map 2,000 ticks: `55125379638382860` ✓
- World 1,800 ticks: `32337437717390864` ✓

## Test plan

- [x] Full suite green (1,905 tests), including updated `units()`
semantics tests (array overload, snapshot isolation, insertion order)
- [x] Hash equality on 3 seeded headless runs (2 maps)
- [x] Before/after 20-min GC benchmarks on the same commit base

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:02:36 -07:00
blon 3c196cb7e7 crit fix: indian subcontinent map crash (#4479)
Resolves #4401

NOTE: While this PR is an improvment, the Indian subcontinent crash IS
NOT caused by >253 water components, as the map only has ~15 water
components.

## Description:

Fixes a critical browser tab crash ("Aw, Snap! Something went wrong")
when loading the game on the new Indian Subcontinent map (or any map
with >= 253 water components) in Solo Mode.

### Technical Cause:
1. When a map contains >= 253 disconnected water components, the array
mapping tiles to component IDs is dynamically promoted from a Uint8Array
to a Uint16Array.
2. This promotion upgrades the land sentinel LAND_MARKER from 0xff (255)
to LAND_MARKER_WIDE (0xffff / 65535).
3. The BFS local search filter in AStarWaterHierarchical had a hardcoded
sentinel check: (t: TileRef) => this.graph.getComponentId(t) !==
LAND_MARKER (evaluating against 255).
4. On promoted maps, land tiles (65535) matched this check as water. The
local BFS then traversed the entire landmass of the map, resulting in
CPU exhaustion and memory/stack overflows that crashed the rendering
process.

### Solution:
Changed the hardcoded sentinel check to query the map's terrain directly
via this.map.isWater(t). This makes the check immune to any component ID
promotions or sentinel representation upgrades. Verified that the
existing water pathfinding test suite passes successfully.

## Please complete the following:

- [ ] I have added screenshots for all UI updates (N/A)
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file (N/A)
- [x] I have added relevant tests to the test directory (Existing tests
in tests/core/pathfinding/PathFinding.Water.test.ts cover water
pathfinding behavior and run successfully)

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

blontd6
2026-07-03 12:45:30 -07:00
Evan 22d5aba5ae refactor: standardize cardinal-neighbor iteration on neighbors() N,S,W,E order (#4495)
## Summary

Follow-up to #4494. That PR added `forEachNeighborNSWE` as a third
neighbor iterator because the existing allocation-free helpers
(`forEachNeighbor`, `neighbors4`) visit in W,E,N,S order while
`neighbors()` visits N,S,W,E — and substituting one for the other
changes simulation behavior at order-sensitive call sites.

This PR removes that duplication by standardizing on **one order
everywhere**: `forEachNeighbor` and `neighbors4` now visit in the same
N,S,W,E order as `neighbors()`, and `forEachNeighborNSWE` is deleted.

## ⚠️ Intentional behavior change

Callers of the flipped helpers that are order-sensitive now make
different (equally valid) decisions:

- `AttackExecution.addNeighbors` — PRNG values are drawn per neighbor
while building the conquest frontier, so attack expansion patterns
differ
- `AttackExecution.handleDeadDefender` — a dead defender's tiles go to
the *first-visited* adjacent player
- `WarshipExecution.bestNeighborToward` — distance ties break by visit
order
- `PlayerExecution` surrounded-cluster flood fill — set insertion order
propagates to conquer order

Game outcomes for a given seed differ from previous builds (verified:
the 12k-tick reference run ends with 31 players alive vs 24 before).
Determinism across clients *within* a build is unaffected — all clients
run the same code, so there is no desync risk. Replays/verification
pinned to old hashes will not match this build.

New reference hashes for the headless perf harness (seed
`perf-default`):

| Run | Final hash |
|---|---|
| giantworldmap, 12,000 ticks | `57830793797434300` |
| giantworldmap, 2,000 ticks | `55125379638382860` |
| world, 1,800 ticks | `32337437717390864` |

## Verification

- [x] Full suite green (1,901 tests), including new exact-order contract
tests: `forEachNeighbor` and `neighbors4` must match `neighbors()`
contents **and order** for every tile
- [x] 20-game-minute Giant World Map benchmark: no perf regression (73
ticks/sec, GC 1.2% of wall, allocation profile unchanged)
- [x] Order-sensitivity audit of every `forEachNeighbor`/`neighbors4`
call site (sensitive ones listed above; the rest are booleans, counts,
or min/max accumulations)

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:42:22 -07:00
Evan 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
Evan 5e4b2791aa perf: reduce core-sim GC churn 42% and add GC-churn profiling to the perf harness (#4494)
## Summary

Reduces core-simulation GC churn by **42%** on a 20-game-minute Giant
World Map run, and extends the headless full-game perf harness so churn
is measurable and regressions are visible.

### 1. GC-churn measurement (`tests/perf/fullgame/GcProfiler.ts`)

`npm run perf:game` now reports:

- **GC pauses** by kind (minor/major/incremental) via a
`PerformanceObserver` on `'gc'` entries, bucketed into tick windows by
timestamp (V8 only delivers these entries on a timer task, so they're
flushed after the run)
- **Allocation rate** per `--window N` ticks (default 1000) from
used-heap deltas sampled every tick, so churn can be tracked across game
phases
- **Top allocating functions** from the V8 sampling heap profiler with
`includeObjectsCollectedBy{Major,Minor}GC` — i.e. actual churn including
short-lived garbage, not live memory — plus a `.heapprofile` loadable in
Chrome DevTools (Memory → Allocation sampling)

New flags: `--window N`, `--no-gc-profile`, `--no-alloc-profile`.

### 2. Allocation reductions in the hot paths it found

| Site | Change |
|---|---|
| `GameMap.bfs` | inline neighbor enumeration instead of an array per
visited tile |
| `GameMap`/`Game` | new `forEachNeighborNSWE` — allocation-free
iterator matching `neighbors()` N,S,W,E order for order-sensitive
callers (`forEachNeighbor` visits W,E,N,S, so substituting it would
change sim behavior) |
| `PlayerImpl.nearby` / `sharesBorderWith` / `shoreReachableNeighbors` |
no per-call neighbor arrays; no materialized shore-tile array |
| `PlayerImpl.units(types)` | gather into a reusable scratch buffer,
return one exact-size slice (still a fresh snapshot array per call) |
| `AiAttackBehavior.maybeAttack` | single pass over border neighbors
replacing the `flatMap`/`filter`/`map` chain over every border tile |
| `AiAttackBehavior.isBorderingNukedTerritory` | reusable `neighbors4`
buffer with early exit |
| `SharedWaterCache.build` | allocation-free neighbor iteration |
| `SpatialQuery.bfsNearest` | first-minimum scan instead of
collect-then-stable-sort (identical result incl. tie-breaking) |

### Results (Giant World Map, 400 bots, 12,000 ticks ≈ 20 game-minutes,
seed `perf-default`)

| Metric | Before | After |
|---|---|---|
| Sampled allocations (incl. collected) | 97.7 GB | **56.9 GB (−42%)** |
| GC count / total pause | 1,682 / 3,313 ms (1.8% of wall) | 1,058 /
2,087 ms (1.2%) |
| Ticks/sec | 66 | 70 |
| p99 / max tick | 49.9 ms / 988 ms | 43.5 ms / 689 ms |
| Ticks over 100 ms budget | 31 | 19 |

## Determinism

Every rewrite preserves exact iteration order (the new NSWE iterator
exists precisely for the order-sensitive sites). Verified by identical
final game-state hashes on three runs: Giant World Map 12,000 ticks
(`67286276735690560`), Giant World Map 2,000 ticks, and World 1,800
ticks.

## Test plan

- [x] Full suite green (1,896 tests)
- [x] New tests: `forEachNeighborNSWE` order contract vs `neighbors()`
over every tile; `units()` filtering semantics (insertion order,
fresh-array guarantee, duplicate types, Set path)
- [x] Final-hash equality on 3 seeded headless runs (2 maps)

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:30:28 -07:00
Zixer1 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
bijx 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
dependabot[bot] 64a6111fd4 Bump the updates group with 2 updates (#4475)
Bumps the updates group with 2 updates:
[actions/checkout](https://github.com/actions/checkout) and
[toshimaru/auto-author-assign](https://github.com/toshimaru/auto-author-assign).

Updates `actions/checkout` from 6 to 7
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>block checking out fork pr for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
<li>getting ready for checkout v7 release by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2464">actions/checkout#2464</a></li>
<li>update error wording by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2467">actions/checkout#2467</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.3...v7.0.0">https://github.com/actions/checkout/compare/v6.0.3...v7.0.0</a></p>
<h2>v6.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Update changelog by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2357">actions/checkout#2357</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>Update changelog for v6.0.3 by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2446">actions/checkout#2446</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/yaananth"><code>@​yaananth</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.3">https://github.com/actions/checkout/compare/v6...v6.0.3</a></p>
<h2>v6.0.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Add orchestration_id to git user-agent when ACTIONS_ORCHESTRATION_ID
is set by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2355">actions/checkout#2355</a></li>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.1...v6.0.2">https://github.com/actions/checkout/compare/v6.0.1...v6.0.2</a></p>
<h2>v6.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update all references from v5 and v4 to v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2314">actions/checkout#2314</a></li>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
<li>Clarify v6 README by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2328">actions/checkout#2328</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.1">https://github.com/actions/checkout/compare/v6...v6.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"><code>9c091bb</code></a>
update error wording (<a
href="https://redirect.github.com/actions/checkout/issues/2467">#2467</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/1044a6dea927916f2c38ba5aeffbc0a847b1221a"><code>1044a6d</code></a>
getting ready for checkout v7 release (<a
href="https://redirect.github.com/actions/checkout/issues/2464">#2464</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/f0282184c7ce73ab54c7e4ab5a617122602e575f"><code>f028218</code></a>
Bump the minor-npm-dependencies group across 1 directory with 3 updates
(<a
href="https://redirect.github.com/actions/checkout/issues/2462">#2462</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/d914b262ffc244530a203ab40decab34c3abf34d"><code>d914b26</code></a>
upgrade module to esm and update dependencies (<a
href="https://redirect.github.com/actions/checkout/issues/2463">#2463</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/537c7ef99cef6e5ddb5e7ff5d16d14510503801d"><code>537c7ef</code></a>
Bump <code>@​actions/core</code> and <code>@​actions/tool-cache</code>
and Remove uuid (<a
href="https://redirect.github.com/actions/checkout/issues/2459">#2459</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/130a169078a413d3a5246a393625e8e742f387f6"><code>130a169</code></a>
Bump js-yaml from 4.1.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2461">#2461</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/7d09575332117a40b46e5e020664df234cd416f3"><code>7d09575</code></a>
Bump flatted from 3.3.1 to 3.4.2 (<a
href="https://redirect.github.com/actions/checkout/issues/2460">#2460</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/0f9f3aa320cb53abeb534aeb54048075d9697a0e"><code>0f9f3aa</code></a>
Bump actions/publish-immutable-action (<a
href="https://redirect.github.com/actions/checkout/issues/2458">#2458</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/f9e715a95fcd1f9253f77dd28f11e88d2d6460c7"><code>f9e715a</code></a>
block checking out fork pr for pull_request_target and workflow_run (<a
href="https://redirect.github.com/actions/checkout/issues/2454">#2454</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v6...v7">compare
view</a></li>
</ul>
</details>
<br />

Updates `toshimaru/auto-author-assign` from 3.0.2 to 3.0.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/toshimaru/auto-author-assign/releases">toshimaru/auto-author-assign's
releases</a>.</em></p>
<blockquote>
<h2>v3.0.3</h2>
<!-- raw HTML omitted -->
<blockquote>
<p>[!NOTE]
This is the first immutable release version. 🔒</p>
</blockquote>
<h2>What's Changed</h2>
<h3>Dependencies</h3>
<ul>
<li>build(deps): bump <code>@​rollup/rollup-linux-x64-gnu</code> from
4.60.2 to 4.60.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/toshimaru/auto-author-assign/pull/167">toshimaru/auto-author-assign#167</a></li>
<li>build(deps): bump <code>@​rollup/rollup-linux-x64-gnu</code> from
4.60.3 to 4.60.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/toshimaru/auto-author-assign/pull/169">toshimaru/auto-author-assign#169</a></li>
<li>build(deps-dev): bump rollup from 4.60.2 to 4.61.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/toshimaru/auto-author-assign/pull/174">toshimaru/auto-author-assign#174</a></li>
<li>build(deps-dev): bump <code>@​rollup/plugin-commonjs</code> from
29.0.2 to 29.0.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/toshimaru/auto-author-assign/pull/172">toshimaru/auto-author-assign#172</a></li>
<li>build(deps): bump <code>@​rollup/rollup-linux-x64-gnu</code> from
4.61.1 to 4.62.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/toshimaru/auto-author-assign/pull/175">toshimaru/auto-author-assign#175</a></li>
<li>build(deps-dev): bump rollup from 4.61.1 to 4.62.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/toshimaru/auto-author-assign/pull/176">toshimaru/auto-author-assign#176</a></li>
</ul>
<h3>Others</h3>
<ul>
<li>docs: bump version to v3.0.2 and add Release badge in README by <a
href="https://github.com/devin-ai-integration"><code>@​devin-ai-integration</code></a>[bot]
in <a
href="https://redirect.github.com/toshimaru/auto-author-assign/pull/166">toshimaru/auto-author-assign#166</a></li>
<li>chore(main): release 3.0.3 by <a
href="https://github.com/github-actions"><code>@​github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/toshimaru/auto-author-assign/pull/177">toshimaru/auto-author-assign#177</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/devin-ai-integration"><code>@​devin-ai-integration</code></a>[bot]
made their first contribution in <a
href="https://redirect.github.com/toshimaru/auto-author-assign/pull/166">toshimaru/auto-author-assign#166</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/toshimaru/auto-author-assign/compare/v3.0.2...v3.0.3">https://github.com/toshimaru/auto-author-assign/compare/v3.0.2...v3.0.3</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/toshimaru/auto-author-assign/blob/main/CHANGELOG.md">toshimaru/auto-author-assign's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2><a
href="https://github.com/toshimaru/auto-author-assign/compare/v3.0.2...v3.0.3">3.0.3</a>
(2026-06-15)</h2>
<h3>Miscellaneous Chores</h3>
<ul>
<li>release 3.0.3 (<a
href="https://github.com/toshimaru/auto-author-assign/commit/8f813140fc4dbdfcc4a43f89d97033b5988e3773">8f81314</a>)</li>
</ul>
<h2><a
href="https://github.com/toshimaru/auto-author-assign/compare/v3.0.1...v3.0.2">3.0.2</a>
(2026-04-27)</h2>
<h3>Miscellaneous Chores</h3>
<ul>
<li>release 3.0.2 (<a
href="https://github.com/toshimaru/auto-author-assign/commit/658b95bf703955e926268fcaca1124037270bea8">658b95b</a>)</li>
<li>release 3.0.2 (<a
href="https://github.com/toshimaru/auto-author-assign/commit/ca59fc3261247bab40337927fac848bf2a60863f">ca59fc3</a>)</li>
</ul>
<h2><a
href="https://github.com/toshimaru/auto-author-assign/compare/v3.0.0...v3.0.1">3.0.1</a>
(2025-12-25)</h2>
<h3>Miscellaneous Chores</h3>
<ul>
<li>release 3.0.1 (<a
href="https://github.com/toshimaru/auto-author-assign/commit/718d4ed5349747d47952ae841ae03fcbdd74ebea">718d4ed</a>)</li>
</ul>
<h2><a
href="https://github.com/toshimaru/auto-author-assign/compare/v2.1.2...v3.0.0">3.0.0</a>
(2025-12-21)</h2>
<h3>Features</h3>
<ul>
<li>Add <code>npm run package</code> instead of <code>build</code> (<a
href="https://redirect.github.com/toshimaru/auto-author-assign/issues/130">#130</a>)
(<a
href="https://github.com/toshimaru/auto-author-assign/commit/972720f0403d2873e807f16e350c5b0b1be4dda3">972720f</a>)</li>
</ul>
<h3>Miscellaneous Chores</h3>
<ul>
<li>release 3.0.0 (<a
href="https://github.com/toshimaru/auto-author-assign/commit/d100ceff34d1e9cd2c4ea5b8055922f1409f3068">d100cef</a>)</li>
</ul>
<h3><a
href="https://github.com/toshimaru/auto-author-assign/compare/v2.1.1...v2.1.2">2.1.2</a>
(2025-12-16)</h3>
<h3><a
href="https://github.com/toshimaru/auto-author-assign/compare/v2.1.0...v2.1.1">2.1.1</a>
(2024-06-26)</h3>
<h2><a
href="https://github.com/toshimaru/auto-author-assign/compare/v2.0.1...v2.1.0">2.1.0</a>
(2024-01-17)</h2>
<h3><a
href="https://github.com/toshimaru/auto-author-assign/compare/v2.0.0...v2.0.1">2.0.1</a>
(2023-09-26)</h3>
<h2><a
href="https://github.com/toshimaru/auto-author-assign/compare/v1.6.2...v2.0.0">2.0.0</a>
(2023-09-24)</h2>
<h3><a
href="https://github.com/toshimaru/auto-author-assign/compare/v1.6.1...v1.6.2">1.6.2</a>
(2023-01-03)</h3>
<ul>
<li>chore: dependencies update</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/toshimaru/auto-author-assign/commit/3e19bfc990cb1cf0589dce95e9f75289bb1e22de"><code>3e19bfc</code></a>
chore(main): release 3.0.3 (<a
href="https://redirect.github.com/toshimaru/auto-author-assign/issues/177">#177</a>)</li>
<li><a
href="https://github.com/toshimaru/auto-author-assign/commit/8f813140fc4dbdfcc4a43f89d97033b5988e3773"><code>8f81314</code></a>
chore: release 3.0.3</li>
<li><a
href="https://github.com/toshimaru/auto-author-assign/commit/3e0e4f96b40e287b778df5273d0e53c491711daa"><code>3e0e4f9</code></a>
build(deps-dev): bump rollup from 4.61.1 to 4.62.0 (<a
href="https://redirect.github.com/toshimaru/auto-author-assign/issues/176">#176</a>)</li>
<li><a
href="https://github.com/toshimaru/auto-author-assign/commit/d481ffe32108e4f6e6c497c7a4b7ccc809eab186"><code>d481ffe</code></a>
build(deps): bump <code>@​rollup/rollup-linux-x64-gnu</code> from 4.61.1
to 4.62.0 (<a
href="https://redirect.github.com/toshimaru/auto-author-assign/issues/175">#175</a>)</li>
<li><a
href="https://github.com/toshimaru/auto-author-assign/commit/4dccdc964c79c751a5f0570bf807a2baaac70ad1"><code>4dccdc9</code></a>
build(deps-dev): bump <code>@​rollup/plugin-commonjs</code> from 29.0.2
to 29.0.3 (<a
href="https://redirect.github.com/toshimaru/auto-author-assign/issues/172">#172</a>)</li>
<li><a
href="https://github.com/toshimaru/auto-author-assign/commit/1bc6b5cfc226fd7ee62b4cb76dd11fefd5f7cdd4"><code>1bc6b5c</code></a>
build(deps-dev): bump rollup from 4.60.2 to 4.61.1 (<a
href="https://redirect.github.com/toshimaru/auto-author-assign/issues/174">#174</a>)</li>
<li><a
href="https://github.com/toshimaru/auto-author-assign/commit/e2fe17d9d8eb77d9dc3900514e0c29d8db5b9c28"><code>e2fe17d</code></a>
build(deps): bump <code>@​rollup/rollup-linux-x64-gnu</code> from 4.60.3
to 4.60.4 (<a
href="https://redirect.github.com/toshimaru/auto-author-assign/issues/169">#169</a>)</li>
<li><a
href="https://github.com/toshimaru/auto-author-assign/commit/d57829c471f47dcbafaf6f1a43505a01459aa64e"><code>d57829c</code></a>
build(deps): bump <code>@​rollup/rollup-linux-x64-gnu</code> from 4.60.2
to 4.60.3 (<a
href="https://redirect.github.com/toshimaru/auto-author-assign/issues/167">#167</a>)</li>
<li><a
href="https://github.com/toshimaru/auto-author-assign/commit/7a8235eb61f82485956f5f1a70cbc4bbae19f8e7"><code>7a8235e</code></a>
docs: bump version to v3.0.2 and add Release badge in README (<a
href="https://redirect.github.com/toshimaru/auto-author-assign/issues/166">#166</a>)</li>
<li>See full diff in <a
href="https://github.com/toshimaru/auto-author-assign/compare/bdd7688cbf9e6d5683f02f8c7d8ae4062a254b6d...3e19bfc990cb1cf0589dce95e9f75289bb1e22de">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 15:58:17 -07:00
Evan b72956d0c0 Gate users without GPU-accelerated WebGL2 instead of running at ~1fps (#4324)
## Problem

After the WebGL2 renderer migration, a small number of users (~a dozen
of 100k DAU) report ~5fps. Root cause: they run WebGL **without GPU
acceleration** (hardware acceleration disabled, blocklisted driver, or a
locked-down machine), so they get a SwiftShader/software context.
Software-rendered WebGL is hopeless for a real-time game — ~1fps
locally.

We are not supporting a canvas2d fallback. Instead: demand a
GPU-accelerated context, and if we can't get one, **gate** the user with
actionable instructions rather than letting the game crawl.

## What this does

- **`initGL()`**
([initGL.ts](../blob/webgl-software-render-gate/src/client/render/gl/initGL.ts))
— demands `failIfMajorPerformanceCaveat: true` **and** inspects the
unmasked renderer string. The flag alone isn't enough: when hardware
acceleration is turned off in browser *settings* (vs. a blocklisted
driver), Chrome still hands back a SwiftShader context, so we'd
otherwise run at 1fps. Classifies the outcome as `ok` / `software` /
`unsupported`.
- **`GPURenderer`** throws `GLUnavailableError` on a non-accelerated
context; the game-start `catch` shows the gate and removes the orphaned
canvas.
- **`<webgl-gate>`** Lit component renders a full-screen blocking gate
with per-browser steps (Chrome / Edge / Firefox / Safari) for enabling
hardware acceleration / WebGL.
- **`gl_init` analytics event** fires every session (`status` +
`renderer` for non-ok) via the existing Google Tag, so we can size the
real affected % within a day.

## Notes / decisions

- The gate copy is **intentionally inlined (not translated)** — it's a
rarely-seen, browser-specific troubleshooting screen; 28 Crowdin keys
would be poor cost/benefit, and a non-English user still has to navigate
English browser menus.
- `showGLGate` lazy-loads the component (`import()`), so the `render/gl`
module that `Renderer.ts` imports doesn't statically pull a UI component
into its graph.

## Update: fingerprint-capped contexts (#4357)

A third failure class, integrated after the initial PR:
`privacy.resistFingerprinting` (default-on in LibreWolf and Mullvad
Browser, opt-in in Firefox) caps `MAX_TEXTURE_SIZE` at 2048 on an
otherwise hardware-accelerated context. The renderer unconditionally
allocates a 4096-wide palette texture, so the oversized `texImage2D`
calls fail silently and the whole map renders **black** (#4357).

- `initGL` now reads `MAX_TEXTURE_SIZE` after the software check and
classifies the context as **`limited`** when it's below
`getPaletteSize()` (4096 — the hard floor every game needs).
- Unlike `software`/`unsupported`, **`limited` is a warning, not a hard
block**: `initGL` still returns the context, the game starts normally,
and the gate is shown with a "Continue anyway" button. `GPURenderer`
exposes the capped renderer/size via `glLimited` (surfaced through
`MapRenderer`), which `ClientGameRunner` uses to show the warning and
log analytics.
- The gate shows fingerprinting-specific instructions for `limited` (add
the site to `privacy.resistFingerprinting.exemptedDomains` in
`about:config`) instead of the hardware-acceleration steps.
- `gl_init` reports `max_texture_size` alongside the renderer for this
status, so we can size the RFP-affected population too.

Fixes #4357

## Test plan

- [x] Unit tests for `initGL`'s `ok` / `software` / `unsupported`
branching, incl. the "returns a context but renderer is software" case
(`tests/client/initGL.test.ts`).
- [x] lint / prettier / tsc clean.
- [x] **Verified in real browsers (macOS).** All three gate states
reproduced:
- `software`: Chrome with `--use-gl=angle --use-angle=swiftshader`
(confirmed "Software only" at `chrome://gpu`), and Chrome with hardware
acceleration toggled off in settings — both show the hard gate instead
of a 1fps game.
- `unsupported`: Firefox with `webgl.disabled=true` shows the
unsupported gate.
- `limited`: Firefox with `privacy.resistFingerprinting=true`
(MAX_TEXTURE_SIZE capped to 2048, same as LibreWolf's default) shows the
dismissible warning; "Continue anyway" starts the game, and exempting
the site via `privacy.resistFingerprinting.exemptedDomains` removes the
warning.

## Acceptance criteria

- Accelerated users: unchanged.
- Software / no-accel users: see the enable-acceleration gate, not a
1fps game.
- No-WebGL2 users: see the unsupported gate.
- `gl_init` fires every session with status (+ renderer for non-ok).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 15:54:06 -07:00
Evan b6317964a7 feat: sparkles nuke-explosion visual type (#4490)
## Description:

Follow-up to #4485: adds a second nuke-explosion visual, `"sparkles"` —
a firework burst of twinkling glints that start at the detonation point
and ride outward with the expanding front, reaching the cosmetic's full
`size` at fade-out.

**Schema (`CosmeticSchemas.ts`)**
- `NukeExplosionAttributesSchema` is now a discriminated union on `type`
(`"shockwave" | "sparkles"`), matching `TrailEffectAttributesSchema`.
Old clients drop sparkles entries via `lenientRecord` and render the
default ring.
- The sparkles member adds `density` (required, positive) — roughly the
total number of glints in the burst.
- Literal attribute semantics, consistent with shockwave:
  - `size` — final burst width (diameter) in world tiles at fade-out
- `speed` — tiles/s the width grows; duration = size / speed, clamped
0.1–15 s
- `thickness` — **average** sparkle size in tiles; each glint
hash-varies ±50% around it
  - `density` — approximate glint count; renderer clamps to 2–5000
- `colors` + `transitionSpeed` — shared palette-cycle semantics, with a
hashed per-glint palette offset on top

**Rendering**
- `NukeExplosionRenderParams` now carries the visual type through to the
pass as a matching TS union (previously any cosmetic was hardwired to
the EMP style — this closes that gap for future visuals).
- Sparkles are style 2 in the same `FxShockwavePass` instance stream:
one new float (grid cell pitch, derived CPU-side from density), no other
layout changes.
- Fragment shader: one hashed glint per rotated front-normalized grid
cell (jittered, cell-confined so each fragment samples only its own
cell, ~1/3 dropout for organic scatter), hashed birth stagger. Glints
are **fully opaque** — twinkle modulates color brightness, not alpha —
holding full opacity through life and fading only over the last quarter.
- SAM interceptions, the classic ring, and EMP shockwaves are unchanged.

**Store / selection**
- New `<sparkles-swatch>` preview (burst scales from center,
density-scaled dot count, size-varied dots, palette cycling), branched
in `CosmeticButton` by `attributes.type`.

**Verification**
- Schema tests incl. the real `rgb_nuke_sparkles` catalog entry,
missing/non-positive `density` rejection.
- Verified in-game via headless Chromium: size-250 RGB burst renders
opaque red/white/blue glints expanding from the detonation point; sparse
(40) vs dense (400) density comparison; no page errors.

## 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 15:21:26 -07:00
crunchybbb 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
bijx 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
Evan 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