## Description:
Previously, every nation in a map's manifest.json required explicit
coordinates. Additional nations already supported optional coordinates
to trigger random spawn placement, but regular nations did not.
Idea from PlaysBadly.
Reasoning (copied off discord):
> I've been working on World Inverted by adding realistic 'nations' in
the form sunken ship names with their flags and location. However after
searching around for other possible nation locations that are ocean
related I realised that I might not have enough info for proper
'realisitc' coverage of the map. Currently Im at ~170 nations with
cordinates. This is not including the additional nations with no
locations. This will be reduced to ~62 as the default with the rest
turning into additional nations.
>
> The problem is the end process is proving difficult. Trying to blance
the nation placment on the map is a little much at this volume. So being
able to add a few no-cordinate nations would be a great way to fill in
the map.
This PR also improves the MapConsistency test to check the additional
nations too.
## 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:
FloPinguin
Attack troop labels, the ghost-cost chip, and the bonusPopup
minScreenScale floor were dividing by `zoom`, which Camera.ts stores
as device-pixels-per-world-unit (canvasW = cssWidth * dpr). The result
was constant device-pixel size — labels rendered ~2x larger on DPR=1
displays than on retina. Multiply each screen-relative scale by dpr so
the on-screen size stays constant in CSS pixels.
Retune the now-correct sizes: attack labels 34 -> 17, ghost-cost
screenScale 30 -> 18, screenYOffset 50 -> 25.
The Start Game button in SinglePlayerModal and HostLobbyModal was
scrolling away with the body instead of staying pinned at the bottom.
HostLobbyModal's body lacked overflow-y-auto/min-h-0, so the body
couldn't scroll independently and the footer scrolled off-screen.
SinglePlayerModal was missing the flex flex-col h-full wrapper entirely
(and had an unmatched closing div), so the footer never had a sticky
column to anchor to.
Restore the v31 layout: outer flex-col h-full, body is the scroll
container (flex-1 min-h-0 overflow-y-auto custom-scrollbar), footer is
shrink-0.
Ports the sound-emission logic from v31's deleted FxLayer into a new
controller registered with the game renderer. Per-tick it scans the
worker's update stream and emits PlaySoundEffectEvent for unit launches,
nuke detonations, structure/warship builds (local player only), and
conquests where the local player is the conqueror.
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #(issue number)
## Description:
adds a check to see if you're in a clan or not. if not, checks to see if
the clan exists, if it does, warns the user, if it doesn't, lets them
use it.
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
w.o.n
Resolves pathfinding issue:
https://discord.com/channels/1359946986937258015/1458870041964445706
## Description:
BucketQueue requires `maxF` to be defined. Estimating it is much more
complicated than the code assumed. This caused the bucket to overflow on
certain paths, mostly (a) twisty paths - defined as one which must
traverse both ways along the same axis, (b) maps where height > width,
because we used `width ** 2` to estimate size instead of `width *
height` (iirc height was not easily accessible).
This PR replcaes BucketQueue with already existing MinHeap.
`AStar.AbstractGraph` is already specialized in traversing
`AbstractGraph`, so I have dropped the `useMinHeap` option and instead
made it the default path. Otherwise we'd be leaving dead code. The max
priority was also fixed to account for duplicate connections, abstract
graph is already very small so it should not affect (and in my testing
does not) affect performance.
**Before**
<img width="1813" height="1781" alt="image"
src="https://github.com/user-attachments/assets/63648db1-a293-441d-8ea8-eaf98694f04a"
/>
**After**
<img width="1734" height="1702" alt="image"
src="https://github.com/user-attachments/assets/b4ec2f68-b945-42e1-bab4-53ba19fa2bbf"
/>
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
moleole
## Description:
- The lobby WebSocket broadcast (`/lobbies`) was re-sending the full
`PublicGames` snapshot — including each lobby's `gameConfig` — to every
connected client every 500ms. Almost nothing in that payload changes
tick-to-tick; only `numClients` moves.
- `WorkerLobbyService` now tracks the sorted set of `gameID`s it last
sent as a full snapshot. On each incoming broadcast it sends a `full`
only when that set changes; otherwise it sends a `counts` delta carrying
just `{gameID → numClients}`.
- This relies on the master-side coupling at
[MasterLobbyService.ts:140-159](src/server/MasterLobbyService.ts#L140-L159):
when master finds a lobby without `startsAt`, it both sets `startsAt`
AND schedules a fresh lobby on the same tick, so the gameID change
brings the `startsAt` (and `gameConfig`) along with it.
- New WS connections are primed with the worker's cached last `full` so
late joiners don't have to wait for the next structural change.
- `LobbySocket` parses the new discriminated union (`PublicLobbyMessage
= full | counts`), keeps the last full snapshot in memory, and merges
counts into it before invoking the existing callback. `GameModeSelector`
is unchanged.
- Master → worker IPC is unchanged — still sends the full snapshot every
500ms. The optimization only applies to the worker → WS-client boundary,
which is the fan-out point.
## 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:
evan
## Description:
When random spawn is active, human SpawnExecutions are pre-created in
GameRunner.init() and fire on the same tick as NationExecution. Because
humans were added first, their SpawnExecution ticked first, called
endSpawnPhase() (in singleplayer), and NationExecution then saw
inSpawnPhase()=false, found the nation not alive, and deactivated it
before ever queuing a SpawnExecution.
Two changes fix this:
1. GameRunner.init(): Move nationExecutions() before spawnPlayers() so
NationExecution ticks first and queues its SpawnExecution before the
human SpawnExecution can end the spawn phase.
2. NationExecution.tick(): After the spawn-phase block, add a guard that
waits when spawnExecAdded is true but the nation hasn't actually spawned
yet. This prevents NationExecution from deactivating on the very next
tick (via !isAlive()) before its queued SpawnExecution has had a chance
to fire and give the nation territory.
I tested it in singleplaye with and without random spawn and also in
public lobbies. Nations now always spawn.
## 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:
FloPinguin
## Description:
- The renderer no longer knows what "dark mode" is.
`RenderSettings.dayNight.mode` (`"light" | "dark"`) is gone — passes
read neutral values (`lighting.ambient: number`, `lighting.enabled:
boolean`).
- `render-settings.json` holds the light-mode baseline. Dark mode is
just another override layer, applied the same way as graphics settings
(`darkNames`, `classicIcons`, etc.).
- New `src/client/render/gl/RenderOverrides.ts` exposes two in-place
mutators with matching shapes:
- `applyGraphicsOverrides(settings, overrides)` — replaces the old
`generateRenderSettings`
- `applyDarkModeOverride(settings, isDark)`
- `ClientGameRunner` regenerates the live settings each time the user
setting changes via `deepAssign(live, createRenderSettings())` + the
override chain. No per-slice copy list, no intermediate object — adding
a new override that touches a new section just works.
- Renamed `dayNight` → `lighting`; collapsed `nightAmbient`/`dayAmbient`
into single `ambient`; renamed `enableLightCompositing` → `enabled`.
- Bumped dark-mode ambient from 0.15 → 0.35 so terrain stays readable.
<img width="1250" height="846" alt="Screenshot 2026-06-02 at 11 47
28 AM"
src="https://github.com/user-attachments/assets/b41e8ffb-6011-4ba0-9e1f-c2a21ff90794"
/>
## 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:
evan
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #(issue number)
## Description:
Restore the localStorage-driven dev pattern override for development
environments
## 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
**Add approved & assigned issue number here:**
Resolves#2549
## Description:
Themes are purely for the client's rendering, and the server doesn't
need context on them. This PR moves `Theme.ts` from
`src/core/configuration` to `src/client/theme` and moves affiliation
colors to `render-settings.json`.
This is to support the ability to add additional themes more quickly,
such as colorblind-friendly themes. No visible changes occur from this
refactor.
## 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:
jetaviz
---------
Co-authored-by: Josh Harris <josh@wickedsick.com>
> **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#4059
## Description:
Makes ghost radius centred on building being upgrading (if upgrading
building)
<img width="1920" height="1080" alt="Screenshot from 2026-06-01
15-45-37"
src="https://github.com/user-attachments/assets/9cf19e59-0e20-4b43-b65f-35eebb37fa8e"
/>
<img width="1920" height="1080" alt="Screenshot from 2026-06-01
15-45-24"
src="https://github.com/user-attachments/assets/1953238f-5156-4bf7-aa18-8278c27d1a68"
/>
## 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:
tktk123456
> **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#2668
## Description:
Makes radius that factories effect equal to the max distance that a
railroad can be and enable factory ghosts
<img width="1920" height="1080" alt="Screenshot from 2026-06-01
20-13-58"
src="https://github.com/user-attachments/assets/1e2f4de8-79fc-4034-b6ad-3c71255c0410"
/>
## 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
Resolves#4098
## Description:
Adds Southeast Asia map for v32. Very requested map. 31 default nations
(with an extra 31 named for HvN).
Map for intense warship and naval warfare with many, many islands. Also
adds flags of the region to be used by nations in the map. More info
specified in issue
https://github.com/user-attachments/assets/b4151db4-825a-4c1c-8bf8-7b760ae056d2
## 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
Resolves#1793
## Note:
@camclark also has a PR (#4073) for this issue and his design is really
nice. I hope his PR gets merged. I opened this PR because I already had
a simple solution which I made a few days ago (just 25 lines). If for
some reason his PR doesn't get merged I propose this as an alternative.
Thanks.
## Description:
Add real-time countdown ETA for boats.
<img width="516" height="70" alt="2026-06-01_00-06"
src="https://github.com/user-attachments/assets/82d7ee7c-b0a3-44c8-9999-799e483c2f69"
/>
## 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:
goose126
Resolves#4094
## Description:
In Free-For-All (FFA) mode where teams default to 0, player isOnSameTeam
checks returned false for oneself, allowing players to attack
themselves. Consequently, if a bot conquered the targeted tile between
queueing a transport ship action and its actual initialization, the
target became itself, causing the bot to execute a self-invasion.
This fix adds a reflexive check in PlayerImpl.ts's isFriendly method to
always treat oneself as friendly. It also adds a safety guard in
TransportShipExecution.ts's init method to abort ship execution if the
target has shifted to the attacker.
## 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:
barfires
`QuickChatExecution` had no cooldown, allowing a player to spam
quick-chat intents and flood a recipient's chat UI. This could bury
incoming alliance request notifications, preventing them from being seen
or accepted.
This fix mirrors the existing emoji cooldown pattern:
- Added `quickChatCooldown()` to `Config` (default: 30 ticks / 3 seconds)
- Added `canSendQuickChat(recipient)` and `recordQuickChat(recipient)`
to `Player` / `PlayerImpl`, tracking outgoing chats per recipient
- `QuickChatExecution.tick()` now checks `canSendQuickChat` before
displaying and records before the display calls (so the cooldown is
always written even if display throws)
Show bonus amount on currency packs
- Add `bonusAmount` field to `PackSchema` (non-negative int)
- Render a rotated green corner ribbon (`+X FREE!`) on pack tiles when
`bonusAmount > 0`
- Add `cosmetics.free` translation key with `numFree` param
<img width="720" height="359" alt="Screenshot 2026-05-12 at 7 40 12 AM"
src="https://github.com/user-attachments/assets/3dd70fc4-c922-47f4-aee6-055047b58563"
/>
Describe the PR.
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
regression is found:
evan
## Description:
Fixes#4069
Adds map of the Caribbean sea and its islands. Archipelago map with lots
of islands, lots of water and a lot of trade.
This map has multiple large landmasses of similar size to prevent
steamrolls (the largest islands and landmasses are around 30%), and
many, many small islands where players can survive and trade. Players
will have to island hop in order to win. 34 nations of Caribbean
countries and territories, with an extra 28 AdditionalNations for a
total of 62 nations for crowded HvN.
Heavy Island maps are very popular in the broader community and we dont
have one for v32, so i figured it would be nice to have a very requested
and popular world location
570k land tiles, fairly small for a map, would be right placed before
World (600k tiles). Also adds some flags of caribbean regions.
https://github.com/user-attachments/assets/9eae81ec-58eb-4594-89fd-2f95742f8b3a
Terrain source from OpenTopography, already credited. No modification to
the tests are needed for new maps added in Game.ts
## 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
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves#4055
## Description:
Adds a 512*512 onion map with 3 nations (Leafer Confederation, Outer
Enclave and Inner Tribe)
<img width="128" height="128" alt="thumbnail"
src="https://github.com/user-attachments/assets/8d97d8dc-6286-4e79-a459-767c936d49ec"
/>
## 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:
tktk1234567
## Description
Each `TradeShipExecution` / `WarshipExecution` /
`TransportShipExecution` constructed its own `WaterPathFinder`, which
built a full transformer chain wrapping the (already-shared)
`AStarWaterHierarchical`. The chain's `SmoothingWaterTransformer`
allocates its own `AStarWaterBounded` with a 100×100 scratch (~480 KB:
four typed arrays + a MinHeap). With ~300 concurrent ships, that's ~150
MB of duplicated scratch buffers serving identical purposes.
Heap snapshot before:
- `WaterPathFinder` ×309 → 151 MB retained
- `AStarWaterBounded` ×312 (= 3 from the shared HPA + 309 from per-ship
smoothers) → 152 MB retained
- Worker total: 230 MB
## Fix
Cache the transformer chain in a module-level `WeakMap<Game, {version,
chain}>` in `PathFinder.ts`, keyed by `Game` and invalidated when
`waterGraphVersion()` changes. `PathFinding.Water` /
`PathFinding.WaterSimple` and the per-ship `WaterPathFinder` all wrap a
fresh (cheap) `PathFinderStepper` around the shared chain. Each ship
keeps its own stepper for its private path cache.
## Why sharing is safe
- The worker is single-threaded; `findPath` runs synchronously, so no
two callers touch the chain's scratch buffers at the same time.
- `AStarWaterBounded.searchBounded` already uses a `stamp++` pattern to
invalidate stale data — it doesn't care whether the previous caller was
the same instance or a different one.
- All transformers in the chain are either stateless or use the same
stamp-protected pattern.
## Stagger preserved
The per-ship stagger after a water-graph rebuild (so 300 ships don't
re-A* in the same tick) is intact. The chain itself rebuilds once per
version bump; each `WaterPathFinder` still counts down its own
`_staggerCountdown` before replacing its stepper (which invalidates its
cached path and forces re-A* against the new chain).
## Heap snapshot after
- `WaterPathFinder` no longer in top retainers
- `AStarWaterBounded` folded into the single 9 MB
`AStarWaterHierarchical`
- Worker total: 80 MB (≈150 MB freed)
## Please complete the following:
- [x] I have added screenshots for all UI updates (N/A — internal
refactor)
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file (N/A — no user-facing text)
- [x] I have added relevant tests to the test directory (existing tests
cover the behavior — all 1279 still pass)
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Discord
evan
The renderer kept a parallel CONSTRUCTION_DURATIONS table in
src/client/render/GameConstants.ts that had drifted from Config: port
showed as 20 ticks but the simulation builds it in 50, so the bar hit
100% and idled for 30 ticks. SAM/silo cooldown constants were also
stale (120/75 vs Config's 90/90), making the missile-readiness bar
slightly wrong too.
Delete GameConstants.ts entirely. Thread the Config instance through
WebGLGameView → GPURenderer → BarPass / FxPass / FxSpritePass /
WorldTextPass; passes call config.unitInfo(...).constructionDuration,
config.SAMCooldown(), config.deletionMarkDuration(), config.msPerTick()
directly. Add Config.msPerTick() since no method existed for it.
Move the visual-only NUKE_EXPLOSION_RADII (not a game-logic value)
into FxSpritePass where it's used.
## Description:
"A high-stakes naval theater where empires clash over narrow corridors,
bottleneck straits, and heavily fortified shorelines."
Modeled to the exact strategic proportions of the classic Black Sea map,
Yellow Sea shifts the focus of global conflict to East Asia. The map is
defined by its massive central body of water, making naval dominance
absolutely essential for survival. However, unlike wide-open oceans,
control of the Yellow Sea is entirely dictated by its unique coastal
geography.
The Shandong And Liaoning Peninsulas are The definitive feature of the
map. Two massive, opposing peninsulas project deep into the sea, acting
as natural, heavily contestable daggers. They create tight naval choke
points in the central waters while forcing land-based players into
brutal, linear frontlines where every pixel of territory is bought with
blood.
The Continental Rim: A sprawling mainland coast wraps around the
northern and western edges of the map, offering expansive land routes
for players who prefer sweeping land invasions over amphibious assaults.
Scale Class: Medium
Gameplay Style: Naval/Land Hybrid, Tactical Choke Points, Frontline
Bottlenecks
Nations: 8
North Korea South Korea Liaoning Shandong Beijing Hebei Tianjin Jilin
description mostly generated by google gemini 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
- [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 crunchybbbbb
<img width="1660" height="1266" alt="Screenshot 2026-05-24 220103"
src="https://github.com/user-attachments/assets/800c6732-677d-44f1-ba5c-c60da5f199e0"
/>
<img width="1500" height="1152" alt="yellow_sea2"
src="https://github.com/user-attachments/assets/9b3ba34a-3f9c-4485-9235-f953fd07be4c"
/>
Game play video https://youtu.be/IcRPTM0rHM0
---------
Co-authored-by: RickD004 <realtacoco@gmail.com>
The structureSprites toggle was only read by the toggle UIs themselves —
no rendering code ever consulted it. Drops the getter/setter from
UserSettings, both toggle rows (SettingsModal + UserSettingModal), and
the en.json keys. Other-language entries are left for Crowdin to reap.
Adds a "Classic icons" toggle in the structure-icons section of the
Graphics Settings modal. Off (default) keeps today's renderer look;
on switches to a classic style — lighter player-colored shape behind
a dark icon glyph, with 0.75 alpha for a subtle translucent feel.
Exposes the underlying tuning as new render-settings knobs
(`structure.fillDarken`, `borderDarken`, `iconAlpha`, `iconR/G/B`) and
threads them through the structure shader as uniforms, replacing the
previously hardcoded `darken(_, 0.65)` / `darken(_, 0.35)` calls and
the hardcoded white `vec3(1.0)` icon color. The `classicIcons` boolean
in the override schema is the single user-facing knob; the generator
derives the five underlying field values from it. Extends the
ClientGameRunner live-apply path to copy the `structure` slice too,
and adds tests covering the schema and preset derivation.
Adds a single "Name color" toggle (Colored / Black) to the Graphics
Settings modal, backed by a `darkNames` boolean in the override schema
that derives the five underlying name-rendering fields
(fill/outline player-color flags + static outline RGB). Forcing the
outline RGB to 0 in dark mode is what makes the shader's defaultFill
ramp actually render black — flipping the boolean uniforms alone
wasn't enough because the fill is derived from uOutlineColor when
fillUsePlayerColor is false.
Flips the render-settings.json defaults so black names are the
renderer baseline; the modal's no-override state follows the JSON
source of truth. Adds tests covering schema parse behavior and the
generateRenderSettings derivation for each override field.
Chip used a world-space scale and y-offset, so it shrank as the camera
zoomed out and slid under the cursor. Now both the scale and the
downward offset are divided by zoom each frame, mirroring the existing
attack-troop-label pattern. Tunable via ghostCost in render-settings.
## Description:
- Add a user-facing **Graphics Settings** modal accessible from the
in-game Settings menu, with live preview as sliders change.
- First two knobs: **Name Scale** and **Minimum Name Size** (the
name-cull threshold).
- Overrides stored as a single JSON blob in `localStorage` under
`settings.graphics`, validated by a Zod schema
(`GraphicsOverridesSchema`). Future graphics knobs just extend the
schema + slider list.
## How it fits together
- `generateRenderSettings(overrides)` (`RenderSettings.ts`) — pure
function: clones `render-settings.json` defaults, layers overrides on
top, returns a fresh `RenderSettings`.
- `UserSettings.graphicsOverrides()` / `setGraphicsOverrides()` —
read/write the blob; falls back to `{}` on a missing/corrupt entry.
- `ClientGameRunner` listens for
`USER_SETTINGS_CHANGED_EVENT:settings.graphics`, regenerates, and
`Object.assign`s each category into the live `view.getSettings()` slice
so passes pick up the new values on the next frame (no renderer
reconstruction).
- Modal reads defaults straight from `render-settings.json` so there's
no duplication.
<img width="599" height="515" alt="Screenshot 2026-05-28 at 11 18 43 AM"
src="https://github.com/user-attachments/assets/263d7d91-10d8-4a66-a069-10015c735d60"
/>
## 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
The lil-gui render-settings panel (createDebugGui) has been an orphaned
export since the WebGL renderer landed — no caller, no hotkey, no menu
entry. Wire it to a button in SettingsModal that toggles the panel on
and off, and group it with the existing Performance Overlay toggle
under a new "Development Only" section above the Exit Game block.
Wire targetingAlly through GhostPreviewData.rangeWarning so RangeCirclePass
can color the blast-radius circle red instead of white when the ghosted nuke
would trigger a traitor penalty. Restores behavior lost in the canvas→WebGL
migration.
Filter extractAttackRings to the local player's smallID so FxAttackRingPass
only draws rings on the player's own transports. Drop the unused
extractAttackRingsFromIds variant and the dead frame/index.ts barrel
re-exports.
Replace the hard zoom cutoff in RailroadPass with a linear alpha fade
controlled by a new `railFadeRange` setting. Rails (and bridge pixels)
ramp from invisible at `railMinZoom - railFadeRange` to fully opaque at
`railMinZoom`, instead of popping in. Adds a uRailFade shader uniform
and a debug slider.
## Description:
Add image-based territory skins as a new cosmetic type, rendered
alongside the existing 1-bit patterns. Skins render a single PNG
centered on each player's spawn tile — opaque pixels show the skin
(multiplied by team color in team games, raw colors in FFA), transparent
pixels and tiles outside the image bounds fall through to the regular
player palette color.
**Cosmetic plumbing**
- `SkinSchema` in `CosmeticSchemas.ts`, optional `skins` map on
`CosmeticsSchema`
- `PlayerSkin`, `PlayerCosmetics.skin`, `PlayerCosmeticRefs.skinName` in
`Schemas.ts`
- Server-side resolution: `PrivilegeCheckerImpl.isSkinAllowed` (gated by
`skin:*` / `skin:<name>` flares)
- Client persistence: stored under `PATTERN_KEY` (`pattern:` and `skin:`
share one slot — they're mutually exclusive)
- `getPlayerCosmeticsRefs` only emits a `skinName` when cosmetics are
loaded, the skin exists in the catalog, and the user has the right flare
— otherwise drops the ref and clears storage
**Renderer**
- `SkinAtlasArray` — fixed `TEXTURE_2D_ARRAY`, 1024×1024 per layer,
exact layer count allocated once at game start from the locked-in player
set. No resize, no callbacks, no retained `HTMLImageElement`. Zero GPU
cost when no players have skins (1×1 placeholder).
- `skinLayerTex` (R8UI 4096×1) — per-player `layer + 1` (`0` = no skin)
- `skinAnchorTex` (RG16UI 4096×1) — per-player spawn tile, so the PNG
center anchors at each player's spawn (re-uploads when the player
re-picks during spawn phase)
- `WebGLFrameBuilder.syncPlayers` collects unique skin URLs on first
sync and calls `view.initSkinAtlas(urls)` once; `clearCaches()` resets
so seek/replay re-initializes
- `territory.frag.glsl`: skin branch is mutually exclusive with
patterns; bounds-checks UVs against `[0, 1]` so the image is a single
stamp, not tiled; alpha-blends against the player palette color so
transparent pixels and out-of-bounds tiles render as the regular player
color
**Hover highlight (global UX change, not skin-scoped)**
- Existing hover highlight changed from "brighten toward white" to
"saturation boost." Applies to all players regardless of
skin/pattern/flat-color — looks better across the board.
**UI**
- `CosmeticButton` renders skins as a single `<img>` (object-contain)
- `TerritoryPatternsModal` merges patterns + skins into one grid; single
"default" tile clears both
- Selecting a pattern clears the skin and vice versa (mutually
exclusive)
- `Store` pattern tab includes skin entries (purchasable, not-yet-owned)
- `PatternInput` lobby button previews the active skin when one is set
**Memory**
- 0 skin players → ~4 bytes (placeholder) + ~40 KB fixed per-player
tables
- 1 skin player → ~5.6 MB GPU
- 5 skin players → ~28 MB GPU
- 10 skin players → ~56 MB GPU
**Tests**
- `tests/Privilege.test.ts`: 13 new cases covering `isSkinAllowed`
(wildcard, exact-match, missing flare, missing skin, forged refs) and
`isAllowed` integration (allowed/forbidden paths, short-circuit when
invalid skin is paired with valid other cosmetics)
## Please complete the following:
- [ ] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [ ] 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
Resolves#3959
## Description:
This PR fixes a Denial of Service (DoS) vulnerability in 1v1 matches
related to desync reporting. The `findOutOfSyncClients` logic previously
forced a game-ending desync if half or more players reported conflicting
hashes (`outOfSyncClients.length >= Math.floor(this.activeClients.length
/ 2)`). In a 1v1, this meant a single malicious player sending a bad
hash could trigger a global desync, crashing their opponent's game
session.
The logic has been corrected to require a **strict majority** (`>
Math.floor(this.activeClients.length / 2)`) to declare a lobby-wide
desync. In a 1v1 game, a single malicious actor will now simply be
flagged as the out-of-sync client and disconnected, allowing the honest
player to continue their session uninterrupted.
## 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:
barfires
Co-authored-by: Josh Harris <josh@wickedsick.com>
## Description:
Labyrinth is a maze type map. My attempt at making a more chess style
board for play. Games with bots appear stable at over 45min average run
times. The map has been setup for team spawn zones for 2, 3, 4, 5, 6,
and 7 teams. Some of the team spawns for odd numbers are experimental
and I would like to see how they play out with live players. Additional
nation names included. There are other design factors like each of the
large squares being within the blast radius of a hydro; small islands
are within the blast radius of nukes.
This is meant as a slower playing game. My intentions are to get some
sort of literal rotation of the map in the future if easily implemented.
That way every time players load the game there would be some
randomization.
As an additional note one of my last edit to the map was the "+" shape
to the islands to allow train passage. Zooming out I can see now that
the pattern is squares and + through out. Did not fully intend on that,
but it felt like good vibes.
https://discord.com/channels/1284581928254701718/1293201128858587207/threads/1497062552784605316https://www.youtube.com/watch?v=e8c-TylT4hshttps://www.youtube.com/watch?v=0-yqrfr3nv0
## 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
PlaysBadly
---------
Co-authored-by: RickD004 <realtacoco@gmail.com>
## Description:
Silence noisy console warnings of `LangSelector not found in DOM` on
page hydration.
Previously, `translateText()` queried the DOM very early in the client
rendering/hydration phase before Lit Element had mounted the
`<lang-selector>` element. This resulted in hundreds of noisy warnings
in the browser developer console on page load.
This fix resolves the issue gracefully in Utils.ts: it queries
`getCachedLangSelector()` and immediately returns the key if not found
without polluting the console log with warnings. Once the element is
fully mounted, normal translation and cache updates resume.
## 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:
barfires
## Description:
Add map of korean peninsula. Size 1092x2149
Nations: 35
based on provinces
## 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
crunchybbbbb @crunchybbbbb_59469
<img width="1092" height="2149" alt="Korea-2"
src="https://github.com/user-attachments/assets/874100e8-4a68-4f57-b2f7-49aa87b8194d"
/>
two teams nations war video https://www.youtube.com/watch?v=n4h7GAfAHTM
---------
Co-authored-by: Ricky G.P. <realtacoco@gmail.com>
## Description:
The unit fragment shader (unit.frag.glsl) checked vFlags > 1.5 to
colorize warships in their attacking/angry solid red territory band
(FLAG_ANGRY = 2.0). However, trade-friendly ships use
FLAG_TRADE_FRIENDLY = 3.0 which also matched this condition, causing
friendly/trade-allied ships to incorrectly render as hostile/angry
warships in normal camera view.
This fix refactors unit.frag.glsl to use precise float range queries via
abs() to verify FLAG_ANGRY and FLAG_FLICKER flags specifically,
preventing the trade-friendly flag from triggering the angry-red
colorization.
## 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:
barfires
## Description:
Capturing defense posts previously demoted their level by 1, and
transferred ownership to the invading player if their level was still
above 0. The expected strategic behavior is that defense posts should
always be destroyed (deleted) upon capture.
This fix updates PlayerExecution.ts's structure tick loop to immediately
destroy the Defense Post unit via u.delete(true, captor) instead of
transferring ownership. It also rewrites the corresponding unit tests in
PlayerExecution.test.ts to verify the complete destruction of Defense
Posts of all levels (including level 2+) when the tile owner changes.
## Please complete the following:
- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
barfires
## Description:
Adds `stale-if-error=86400` to the `Cache-Control` header set on the
rendered app shell (`/`) in
[src/server/RenderHtml.ts](src/server/RenderHtml.ts). This lets shared
caches (CloudFlare, nginx `proxy_cache`) keep serving the last good
`index.html` for up to 24h if origin returns a 5xx, alongside the
existing `stale-while-revalidate` window.
Pairs with enabling HTML caching for the `/` route on CloudFlare in
"respect origin headers" mode — it already honors `s-maxage` (5 min edge
TTL) and `stale-while-revalidate`; this just extends the same safety net
to origin-error cases.
No behavior change for successful responses; browsers still revalidate
every load via `max-age=0`.
## 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:
jish
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Names now follow the player's currently-selected spawn tile each tick
instead of going through placeName, which read a cluster bounding box
that only refreshed every cluster-calc cycle and was further throttled
to every other tick. Adds placeSpawnName and switches the spawn-phase
branch in GameRunner to use it.
## Description:
- Added optional `disableClanTags` to `GameConfig`. When set, the server
strips clan tags from `gameInfo` (lobby broadcasts/HTTP) and
`gameStartInfo` (start payload) before sending to clients. Archive keeps
the originals.
- Enabled `disableClanTags` for public FFA games (both the regular FFA
playlist and special when randomized to FFA). No UI; clients still see
their own clan tag via local input state.
## 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:
Replaces the DOM-based `AttackingTroopsOverlay` with
`AttackingTroopsController`, rendering attack troop counts through
`WorldTextPass` instead of a separate fixed-position DOM container.
## Summary
- New `AttackingTroopsController` polls `attackClusteredPositions()`
every 200ms and pushes labels to the WebGL view each frame, lerping
cluster positions over 250ms for smooth front-line movement (replaces
the old CSS `transform 0.25s` transition).
- `WorldTextPass` gains `setAttackTroopLabels()` and renders them at a
fixed on-screen size (zoom-independent) using `screenScale / zoom`.
- World text now draws on top of `NamePass` so attack callouts aren't
hidden behind centered player names.
- Fragment shader adds a soft quadratic dark halo around every
world-text label; extent uses the remaining SDF range after the hard
outline so it fades smoothly to zero (no rectangular clipping).
- Deletes `AttackingTroopsOverlay.ts`; existing unit tests repointed to
the controller's exported `alignClusterOrder`.
<img width="369" height="395" alt="Screenshot 2026-05-24 at 4 43 51 PM"
src="https://github.com/user-attachments/assets/4dbffe20-77f9-4c0f-b956-ecf543538f8d"
/>
## 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:
Adds map of the Balkan Peninsula and surroundings. Heavily requested map
with multiple posts on the Discord all with over 10 or 20 upvotes.
23 NPC/Nations based on countries and relevant regions of the area. Adds
an extra 39 nations for crowded Humans vs Nations gamemode for a total
of 62 NPCs, based on regions of multiple countries. Also some flags for
some regions.
Source from NASA DEM, already credited
Photo of base map, and 62 HvN:
<img width="614" height="588" alt="Captura de pantalla 2026-05-24
030105"
src="https://github.com/user-attachments/assets/5742a4c3-1b1f-4ca7-858d-91529861dd81"
/>
<img width="548" height="547" alt="image"
src="https://github.com/user-attachments/assets/758d8ad0-1515-41b8-8d42-14e76cdd54ed"
/>
This map completes the quartet row of "polemic" maps for v32
<img width="678" height="119" alt="image"
src="https://github.com/user-attachments/assets/9e6f4ef1-f0cc-48ea-a59f-b7ff69033b73"
/>
## 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:
Stop deleted missile silos from reappearing after cooldown expiry
before
https://github.com/user-attachments/assets/714d3580-b60d-479e-8e45-6ee065b79d54
## 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