Skip to content

Sync with upstream and condense Store pattern tiles with alternate colour swatches#1

Open
devin-ai-integration[bot] wants to merge 189 commits into
mainfrom
devin/1777993361-sync-and-condense-store-skins
Open

Sync with upstream and condense Store pattern tiles with alternate colour swatches#1
devin-ai-integration[bot] wants to merge 189 commits into
mainfrom
devin/1777993361-sync-and-condense-store-skins

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented May 5, 2026

Copy link
Copy Markdown

Description:

This PR does three things:

  1. Syncs with upstream — merges 184 commits from openfrontio/OpenFrontIO main to bring the fork up to date with the live game version.

  2. Condenses Store pattern tiles — Previously, patterns with multiple colour palettes appeared as separate tiles (e.g. the same skin repeated 4+ times in different colours). Now each unique pattern skin appears as a single tile with small colour palette swatches below the preview. Clicking a swatch updates the preview and purchase action to that colour variant.

  3. Adds skin preview button — Each pattern tile now has a "Preview" button. Clicking it opens a modal showing the pattern applied to a territory-like blob shape on a dark game background, giving users a sense of how the skin looks in-game before purchasing.

Changes:

  • src/client/Store.ts — Added groupPatternsByName() to group resolved pattern cosmetics by their base pattern name, and passes alternateColors to each cosmetic-button.
  • src/client/components/CosmeticButton.ts — Added alternateColors property, selectedColorIndex state, colour swatches, and a "Preview" button for pattern tiles.
  • src/client/components/SkinPreviewModal.ts — New component that renders an in-game territory preview using the pattern applied to an SVG blob shape on a dark background.
  • resources/lang/en.json — Added store.preview and store.preview_label translation keys.

Please complete the following:

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

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

JohnYuki

Link to Devin session: https://app.devin.ai/sessions/ecabaf1b5754458b8a1f7bf6a355df7f
Requested by: @johnyuki

evanpelle and others added 30 commits April 2, 2026 14:44
## Description:


https://github.com/user-attachments/assets/f2216dec-72aa-497a-89cc-169c2a40021e


* Fortnite-style rarity system for cosmetics: New CosmeticContainer
component applies tier-based visual styling (gradient backgrounds,
glowing borders, hover effects) to flag and pattern cards across 5
rarity tiers: Common, Uncommon, Rare, Epic, and Legendary
* Legendary hover effects: Scale-up animation, pulsing orange glow,
shimmer sweep, rotating border sweep, corner sparkles, and screen
dimming backdrop
* Epic hover effects: Purple shimmer sweep glint on hover
* Purchase button overhaul: Green ember particles on container hover
(non-common only), 40-particle burst stream on button hover, pulsating
green glow, shimmer streak animation, and loading spinner on click
* Clickable cosmetic cards: Clicking anywhere on a purchasable card (not
just the purchase button) triggers the purchase flow
* Refactored components: ArtistInfo renamed to CosmeticInfo (now shows
rarity and color palette in tooltip),
* Forward-compatible rarity schema: rarity field uses .or(z.string()) so
unknown backend values won't break the client


## Please complete the following:

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

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

evan
## Description:

Use admiral to detect adblock and ask users to disable

<img width="1120" height="541" alt="Screenshot 2026-04-06 at 11 46
09 AM"
src="https://github.com/user-attachments/assets/0796b4c4-1e50-475f-b8b7-b95b9ef0f25b"
/>

## Please complete the following:

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

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

evan
## Description:

The deduper was converting profane words like "kkk" => "k" and then
censoring all usernames with the letter "k", so instead we just hardcode
and check for substrings for profane phrases like that.

## Please complete the following:

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

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

evan
…rontio#3601)

## Description:

<img width="3608" height="1848" alt="image"
src="https://github.com/user-attachments/assets/86074bff-e648-4db4-a3e9-08d49e433df0"
/>

## Please complete the following:

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

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

evan
…o#3540)

## Description:

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

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

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

### Contents

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

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

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

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

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


**BEFORE**

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

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

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

## Please complete the following:

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

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

tryout33
…ctory (openfrontio#3605)

Bumps the npm_and_yarn group with 1 update in the / directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).

Updates `vite` from 7.3.0 to 7.3.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v7.3.2</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v7.3.1</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v7.3.1/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v7.3.1...v7.3.2">7.3.2</a>
(2026-04-06)<!-- raw HTML omitted --></h2>
<h3>Bug Fixes</h3>
<ul>
<li>avoid path traversal with optimize deps sourcemap handler (<a
href="https://redirect.github.com/vitejs/vite/issues/22161">#22161</a>)
(<a
href="https://github.com/vitejs/vite/commit/09d8c903bde12fee2710314d3b42bc789c686df7">09d8c90</a>)</li>
<li>backport <a
href="https://redirect.github.com/vitejs/vite/issues/22159">#22159</a>,
apply server.fs check to env transport (<a
href="https://redirect.github.com/vitejs/vite/issues/22162">#22162</a>)
(<a
href="https://github.com/vitejs/vite/commit/19db0f29c3a3ac4e64cc95c270716c77fd223ad1">19db0f2</a>)</li>
<li>check <code>server.fs</code> after stripping query as well (<a
href="https://redirect.github.com/vitejs/vite/issues/22160">#22160</a>)
(<a
href="https://github.com/vitejs/vite/commit/f8103cc946f137a54e395fe3f5d08e8209231ed6">f8103cc</a>)</li>
</ul>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v7.3.0...v7.3.1">7.3.1</a>
(2026-01-07)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li>add <code>ignoreOutdatedRequests</code> option to
<code>optimizeDeps</code> (<a
href="https://redirect.github.com/vitejs/vite/issues/21364">#21364</a>)
(<a
href="https://github.com/vitejs/vite/commit/9d39d373a7b4e0a93322b70b9dbeb202af06af3e">9d39d37</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vitejs/vite/commit/cc383e07b66d4c5a9768fcb570e0af812cb8d999"><code>cc383e0</code></a>
release: v7.3.2</li>
<li><a
href="https://github.com/vitejs/vite/commit/09d8c903bde12fee2710314d3b42bc789c686df7"><code>09d8c90</code></a>
fix: avoid path traversal with optimize deps sourcemap handler (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22161">#22161</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/f8103cc946f137a54e395fe3f5d08e8209231ed6"><code>f8103cc</code></a>
fix: check <code>server.fs</code> after stripping query as well (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22160">#22160</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/19db0f29c3a3ac4e64cc95c270716c77fd223ad1"><code>19db0f2</code></a>
fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22159">#22159</a>,
apply server.fs check to env transport (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22162">#22162</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/95e8923f35d0252c9f6eb2d5e358c084542706f1"><code>95e8923</code></a>
release: v7.3.1</li>
<li><a
href="https://github.com/vitejs/vite/commit/9d39d373a7b4e0a93322b70b9dbeb202af06af3e"><code>9d39d37</code></a>
feat: add <code>ignoreOutdatedRequests</code> option to
<code>optimizeDeps</code> (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/21364">#21364</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v7.3.2/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=7.3.0&new-version=7.3.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/openfrontio/OpenFrontIO/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…tio#3481)

## Description:

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

### Contains

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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

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

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

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

## Please complete the following:

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

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

tryout33
## Description:
Adds sound effects for approved events from the [sound asset
pack](https://drive.google.com/drive/folders/1KpGYJkmLxipy8XmTeyHf40XDC4P--Ck8?usp=sharing).
15 new sound effects triggered from `FxLayer`, `EventsDisplay`, and
`RadialMenu`. Sounds play even when visual FX are off, so disabling
explosions doesn't kill audio. Unapproved sounds are included as assets
but not wired up yet.

### SoundManager architecture

Reworked `SoundManager` per [maintainer
feedback](openfrontio#1893 (comment))
and [follow-up
review](openfrontio#3394):

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

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

### Approved, wired up in this PR

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

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

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

### Not done, no sound files exist yet

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

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

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

Resolves openfrontio#1893

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

Vimacs on Discord pointed out a heavier than needed DOM load from the
[AttackingTroopsOverlay
PR](openfrontio#3427)

- Caches a single `labelTemplate` in `AttackingTroopsOverlay`, built
once on init and cloned per label instead of recreating it each time
- Removes redundant inline style assignments that were repeated on every
label creation
- Simplifies `updateLabelContent` by accessing template-guaranteed
children directly by index

## Please complete the following:

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

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

Radyus
…tio#3607)

## Description:

### Problem

When a host toggled off certain settings (game length, PVP immunity,
starting gold, gold multiplier, disable alliances) in the host lobby
modal, joiners still saw the old values. The settings appeared "stuck"
once enabled.

### Root Cause

`putGameConfig()` sent `undefined` for disabled settings, but
`JSON.stringify` strips `undefined` properties entirely. The server's
`!== undefined` guard never fired, so the old value was never cleared.

### Fix

- **HostLobbyModal**: Send `null` instead of `undefined` when these
settings are toggled off (`null` survives JSON serialization)
- **Schemas**: Add `.nullable()` to the five affected fields
(`maxTimerValue`, `spawnImmunityDuration`, `goldMultiplier`,
`startingGold`, `disableAlliances`)
- **GameServer**: Use `?? undefined` (nullish coalescing) to convert
incoming `null` back to `undefined` when storing on the config

Other settings are unaffected. Booleans like `infiniteGold` always send
`true`/`false`, and fields like `bots`/`gameMap` always have a concrete
value..

## Please complete the following:

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

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

FloPinguin

---------

Co-authored-by: Evan <evanpelle@gmail.com>
## Description:
Adition of Strait of Malacca map

It adds a new map, requested by
Coolson
awildcoolson
Describe the PR.
The map added is a map inspired by the strait of Malacca, it contains
two large landmasses and some islands
## Please complete the following:

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

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

lerithmercano

---------

Co-authored-by: Ricky G.P. <realtacoco@gmail.com>
Co-authored-by: VariableVince <24507472+VariableVince@users.noreply.github.com>
…penfrontio#3545)

Resolves openfrontio#2998 

## Description:

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

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

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

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


## Please complete the following:

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

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

deathllotus

---------

Co-authored-by: Evan <evanpelle@gmail.com>
## Description:

Put Win Modal on top of other modals, namely the Player Panel (z-index
10001) and EmojiTable (z-index 10002). Because currently if Player Panel
is open when the Win Modal (z-index 9999) appears on death/win, the
Panel is incorrectly shown on top of the Win Modal.

Fix is to up Win Modal z-index to 10010, which also leaves room for
other (newly added) panels/modals below it still.

Fixes:
https://discord.com/channels/1284581928254701718/1284581928833388619/1491504813521895534

![image](https://github.com/user-attachments/assets/31803878-628f-41e5-83a5-7f6a2a6fa884)

## Please complete the following:

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

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

tryout33
…k spawn point (openfrontio#3611)

## Description:

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

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

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

Fixes openfrontio#3609

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

## Please complete the following:

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

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

tryout33

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
## Description:

Remove dead code relating to colors.

## Please complete the following:

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

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

DISCORD_USERNAME

babyboucher
## Description:

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

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

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

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

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

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

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

### Media

Video does not show the updated craters


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

Updated craters (no tiny islands after impact):

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

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

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

## Please complete the following:

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

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

FloPinguin
…uld not update flag on clearFlag (openfrontio#3626)

## Description:

openfrontio#3479 put setFlag and
clearFlag in UserSettings.
openfrontio#3481 did some updates to
incorporate them into UserSettings cache. But made two misjudgments.

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

This PR fixes it.

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

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

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

## Please complete the following:

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

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

tryout33
## Description:

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

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

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


## Please complete the following:

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

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

evan
…penfrontio#3631)

## Description:

Fix for v30 and main.

Do not show "Not logged in" on the FlagInputModal in CrazyGames, since
our own login should not work there. It was added in
openfrontio#3521 in v30 so this fix
is needed for production too.

<img width="1415" height="797" alt="image"
src="https://github.com/user-attachments/assets/ef839e08-827d-4eea-b5aa-8aca6357ad07"
/>

## Please complete the following:

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

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

tryout33
…penfrontio#3631)

## Description:

Fix for v30 and main.

Do not show "Not logged in" on the FlagInputModal in CrazyGames, since
our own login should not work there. It was added in
openfrontio#3521 in v30 so this fix
is needed for production too.

<img width="1415" height="797" alt="image"
src="https://github.com/user-attachments/assets/ef839e08-827d-4eea-b5aa-8aca6357ad07"
/>

## Please complete the following:

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

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

tryout33
## Description:

Adds a currency pack system to the store. Players can purchase packs of
in-game currency (Plutonium and Caps) via Stripe checkout.

What's new:

* Pack schema (PackSchema) — new cosmetic type with currency
(hard/soft), amount, and displayName
* "Packs" tab in the Store — renders purchasable currency packs using
existing CosmeticButton infrastructure
* Stripe checkout flow — new createCurrencyPackCheckout API call and
handlePackPurchase handler
* Currency display in Account modal — shows Plutonium and Caps balances
when logged in
I* con components — <plutonium-icon> (animated green glow + rotate) and
<cap-icon> with new SVG assets
* Currency in UserMeResponse — player.currency.hard /
player.currency.soft added to the API schema

## Please complete the following:

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

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

evan
## Description:
Add map Luna (The Moon).

https://youtu.be/6L6vS9VvD8k


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

## Please complete the following:

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

##  Discord username 
PlaysBadly

---------

Co-authored-by: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
evanpelle and others added 25 commits May 2, 2026 09:33
CSS mask-image triggers a CORS fetch, which failed for the CDN-hosted medal SVG. Switched to a Vite ?raw import so the SVG is embedded as a data URI at build time — no network request, no CORS.

Also stripped the SVG of Inkscape metadata and replaced filter-based color inversion with a plain fill="white", shrinking it from 3,278 → 955 bytes (387 bytes gzipped).
Previously, winnerVotes was keyed only on winner, so a client could send the correct winner with spoofed allPlayersStats and have their vote count toward the majority. The key is now a SHA-256 hash of both winner and allPlayersStats together, so any stats divergence produces a distinct key that must independently reach majority.

When the winner is confirmed, the log now includes votesByKey — a summary of every distinct key, its vote count, and winner value — making stat manipulation visible in the logs.
## Description:

QoL changes for the Bosphorus map.

The base image for the map had a little aliasing which resulted in some
rivers being cut off.

<img width="673" height="386" alt="image"
src="https://github.com/user-attachments/assets/b8ed181f-fbeb-4d6f-b7ab-a8b0ea300a22"
/>

All of the nations except istanbul also had absolutely nothing to do
with the region. For example, Varna is a city in Northeast bulgaria and
the aegean isles are also nowhere near. There was also a nation that did
not spawn because its coordinates were in the sea.

<img width="626" height="413" alt="image"
src="https://github.com/user-attachments/assets/a6537f79-3785-4316-8fd4-a99f55faff71"
/>

All of these poor designs are probably the result of the map being
resized, originally being a larger map. It probably explains why this is
the smallest map by land area (not counting Oceania)

Fixes:

- Fixed landlocked rivers and added More accurate bodies of water

- Complete remake of the nations. Nations are now far more accurately
placed districts of the region, and more evenly placed

<img width="664" height="407" alt="image"
src="https://github.com/user-attachments/assets/ff5a34fc-dea0-4a3d-b798-39d5711b91af"
/>

The data and gamestyle of this map should not change much, as the
landmasses remain in practice the same

## Please complete the following:

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

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

tri.star1011
## Description:

In PR 3654 i left an unnecessary question mark.

## Please complete the following:

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

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

tryout33
## Description:

QoL changes for the Bosphorus map.

The base image for the map had a little aliasing which resulted in some
rivers being cut off.

<img width="673" height="386" alt="image"
src="https://github.com/user-attachments/assets/b8ed181f-fbeb-4d6f-b7ab-a8b0ea300a22"
/>

All of the nations except istanbul also had absolutely nothing to do
with the region. For example, Varna is a city in Northeast bulgaria and
the aegean isles are also nowhere near. There was also a nation that did
not spawn because its coordinates were in the sea.

<img width="626" height="413" alt="image"
src="https://github.com/user-attachments/assets/a6537f79-3785-4316-8fd4-a99f55faff71"
/>

All of these poor designs are probably the result of the map being
resized, originally being a larger map. It probably explains why this is
the smallest map by land area (not counting Oceania)

Fixes:

- Fixed landlocked rivers and added More accurate bodies of water

- Complete remake of the nations. Nations are now far more accurately
placed districts of the region, and more evenly placed

<img width="664" height="407" alt="image"
src="https://github.com/user-attachments/assets/ff5a34fc-dea0-4a3d-b798-39d5711b91af"
/>

The data and gamestyle of this map should not change much, as the
landmasses remain in practice the same

## Please complete the following:

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

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

tri.star1011
…ory capture 🛡️ (openfrontio#3814)

## Description:

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

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

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

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

.

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

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

FloPinguin
…ory capture 🛡️ (openfrontio#3814)

## Description:

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

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

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

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

.

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

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

FloPinguin
…o#3826)

## Description:

`MapLandTiles` was fetching map manifests via HTTP at
`http://localhost:3000/maps/<map>/manifest.json`. This stopped working
after PR openfrontio#3494 moved all public assets from stable paths (e.g.
`/maps/...`) to content-hashed paths under `/_assets/...`. The master
server no longer serves `/maps/` -- requests fell through to the SPA
handler, returned `index.html`, failed JSON parsing, and silently fell
back to the default `1_000_000` land tile count.

With 1M tiles, `calculateMapPlayerCounts` produces `[50, 40, 25]`
instead of the real values (e.g. `[185, 140, 95]` for Alps), causing all
public lobbies to be capped at 25-50 players regardless of map.

Fixes this by reading map manifests directly from disk instead of via
HTTP:
- In production: resolves the source path through
`getRuntimeAssetManifest()` to the hashed file under `static/_assets/`,
then reads it with `fs.readFile`.
- In dev: falls back to `resources/maps/<name>/manifest.json` directly
(the Dockerfile removes `resources/maps` in production, so this branch
only runs locally).

Also adds an in-process cache so each map manifest is only read once per
server lifetime.

Verified that it works on
https://fix-map-land-tiles-asset-manifest.openfront.dev/ and locally.

## Please complete the following:

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

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

FloPinguin
…o#3826)

## Description:

`MapLandTiles` was fetching map manifests via HTTP at
`http://localhost:3000/maps/<map>/manifest.json`. This stopped working
after PR openfrontio#3494 moved all public assets from stable paths (e.g.
`/maps/...`) to content-hashed paths under `/_assets/...`. The master
server no longer serves `/maps/` -- requests fell through to the SPA
handler, returned `index.html`, failed JSON parsing, and silently fell
back to the default `1_000_000` land tile count.

With 1M tiles, `calculateMapPlayerCounts` produces `[50, 40, 25]`
instead of the real values (e.g. `[185, 140, 95]` for Alps), causing all
public lobbies to be capped at 25-50 players regardless of map.

Fixes this by reading map manifests directly from disk instead of via
HTTP:
- In production: resolves the source path through
`getRuntimeAssetManifest()` to the hashed file under `static/_assets/`,
then reads it with `fs.readFile`.
- In dev: falls back to `resources/maps/<name>/manifest.json` directly
(the Dockerfile removes `resources/maps` in production, so this branch
only runs locally).

Also adds an in-process cache so each map manifest is only read once per
server lifetime.

Verified that it works on
https://fix-map-land-tiles-asset-manifest.openfront.dev/ and locally.

## Please complete the following:

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

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

FloPinguin
…io#3831)

## Description:

The store item cards were truncating names with an ellipsis. This change
updates the cosmetic card name label to wrap instead of truncating, so
the full name is always shown.

before
<img width="748" height="363" alt="スクリーンショット 2026-05-04 10 26 58"
src="https://github.com/user-attachments/assets/32030be3-6e92-4ca6-8117-451c0ae75582"
/>

after
<img width="756" height="585" alt="スクリーンショット 2026-05-04 10 27 30"
src="https://github.com/user-attachments/assets/20e0fd36-dea4-4236-852b-ca5a2cd7e0f5"
/>

## Please complete the following:

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

## Please put your Discord username so you can be contacted if a bug or
regression is found:
aotumuri
Resolves
#https://discord.com/channels/1359946986937258015/1381347989464809664/1500830892405424168

## Description:
Fixes a mobile layout issue where a large gap appeared below the
OpenFront logo, causing fewer menu options to be visible without
scrolling.

before
<img width="591" height="910" alt="スクリーンショット 2026-05-04 21 32 32"
src="https://github.com/user-attachments/assets/7d9de0de-8d19-4e54-bec6-2bc3b9dda6a5"
/>

after
<img width="603" height="1311" alt="OpenFront (ALPHA)"
src="https://github.com/user-attachments/assets/e606feee-0f33-4a8c-b100-514005a0d2aa"
/>


## Please complete the following:

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

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

aotumuri

Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
## Description:

Other settings previously included emojis, but those have since been
removed.
This change updates the user settings to keep the UI consistent.

## Please complete the following:

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

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

Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
## Description

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

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

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

## Please complete the following:

- [x] I have added screenshots for all UI updates (n/a — server only)
- [x] I process any text displayed to the user through translateText()
(n/a — no user-visible text)
- [ ] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Discord:

evanpelle
Co-Authored-By: John Yuki <30492756+johnyuki@users.noreply.github.com>
Group patterns that share the same base skin into a single tile.
Each tile shows color palette swatches when multiple color variants
exist, allowing users to preview and purchase any variant from
a single condensed tile.

Co-Authored-By: John Yuki <30492756+johnyuki@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration Bot and others added 3 commits May 5, 2026 15:24
Adds a 'Preview' button on pattern tiles in the Store. Clicking it opens
a modal showing the pattern applied to a territory-like blob shape on a
dark game background, giving users a sense of how the skin looks in-game
before purchasing.

Co-Authored-By: John Yuki <30492756+johnyuki@users.noreply.github.com>
Renders a canvas-based preview showing multiple player territories on a
dark ocean background, with the selected skin applied to the central
territory. Uses the actual PatternDecoder for pixel-accurate pattern
rendering, giving users a realistic sense of how the skin looks in-game
alongside other players.

Co-Authored-By: John Yuki <30492756+johnyuki@users.noreply.github.com>
Replace the generated game scene with the existing GameplayScreenshot.png
as the preview background. A territory mask identifies the green territory
in the France/Spain region where the selected skin pattern is composited
on top, giving users a realistic view of how the skin looks in an actual
game alongside other players.

Co-Authored-By: John Yuki <30492756+johnyuki@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.