Commit Graph

2120 Commits

Author SHA1 Message Date
evanpelle b5840d7887 Fix troop count precision in name labels, throttle/stagger updates
Replace the hand-rolled formatTroops() in the name-pass with the canonical
renderTroops() so map name labels match troop precision used elsewhere in
the UI (Leaderboard, PlayerPanel, etc.).

Also refresh each player's troop string at most every 500ms instead of
every simulation tick, staggered by slot index so GPU string uploads
spread across the window rather than bursting on a single tick.
2026-06-09 19:35:32 -07:00
evanpelle cb9cab9aca Keep static spawn timer for singleplayer games
PR #4198 made the spawn-phase timer count down numSpawnPhaseTurns(), but
singleplayer never adds SpawnTimerExecution (GameRunner.ts), so its spawn
phase doesn't end on a timer — it ends when the player spawns. The
countdown would tick to 0 at ~10s while the phase kept going.

In GameRightSidebar.tick(), restore the old static display (maxTimerValue
* 60, or 0 when unset) during spawn phase for Singleplayer games, leaving
the countdown for all other game types. Uses an explicit gameType check
rather than _isSinglePlayer so replays of multiplayer games still count
down.
2026-06-09 19:22:11 -07:00
evanpelle 2d28d5463b Add territory saturation and opacity graphics settings
Expose two new user-configurable map-overlay controls in the graphics
settings modal: territory saturation (mutes fill colors toward grayscale)
and territory opacity (lets terrain show through the fill).

The territory fragment shader blends the fill toward its luminance based
on uSaturation and applies uTerritoryAlpha as the absolute fill opacity.
Both are wired through RenderSettings, the GraphicsOverrides schema,
applyGraphicsOverrides, the debug Layout sliders, and TerritoryPass
uniforms, with defaults (saturation 1, alpha 0.588) in render-settings.json.
Adds the corresponding en.json label/description strings.
2026-06-09 19:16:04 -07:00
Blake Girardet 90e4dd0677 Fixes malformed flag svg url in playerRow (#4203)
Resolves #4194 

## Description:

Fixes the malformed flag svg link when viewing the player row component.

This has been tested by temporarily registering a route to the game-info
modal locally and confirming the flag svg now loads.

Local before

<img width="698" height="500" alt="image"
src="https://github.com/user-attachments/assets/a5bd0958-e4f2-4ab6-9203-b49e42a34ca7"
/>

---
Local after

<img width="770" height="573" alt="Screenshot 2026-06-09 at 6 56 17 PM"
src="https://github.com/user-attachments/assets/ffc64c50-f0d9-4c22-9325-34924b68c985"
/>

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

Caidora
2026-06-09 18:39:19 -07:00
evanpelle 65e99b25e7 Add retreating warship indicator and warship 2-color treatment
Warships now render with a dedicated center accent band so their state
reads at a glance:
- Normal: center + outer ring share the territory color (2-color look),
  hull uses the border color.
- Angry (attacking): outer ring and center turn red.
- Retreating to repair: the center blinks black.

The warship sprite center moved to its own gray value (100) in the unit
atlas so the shader can drive it via a new fourth replacement band, with
no per-unit-type branching — the missiles' shared 130 blend band is
untouched.

Warship repair-retreat (warshipState.state === "retreating") now feeds
the existing UnitState.retreating boolean in UnitView, which UnitPass
maps to a FLAG_RETREATING instance flag.
2026-06-08 17:32:21 -07:00
evanpelle 611560a0b2 Restyle spawn-phase self highlight: gold instead of white
The local player's spawn ring was plain white, which was hard to see
against white mountain terrain. Recolor it to a bright gold so it stays
visible regardless of the terrain underneath, and make it stand out more
overall during the spawn phase:
- Recolor the self ring from white to a bright gold tint
- Keep the ring center transparent so your territory shows through,
  ramping up to solid at the inner edge
- Raise the breathing opacity floor (35% -> 65%) so the ring stays more
  solid through the dim part of the pulse
- Speed up the breathing animation and enlarge the self ring radii
2026-06-08 16:04:28 -07:00
evanpelle 99a20ac032 Shrink warship shell sprite to a single pixel
The shell (unit-atlas.png col 7) was a centered 3×3 white square, so it
rendered as a 3×3-world-tile block. Replace it with a single centered
white pixel so shells render as one pixel, matching the original
pixel-shell look. The atlas is sampled with NEAREST and unitSize is 13
(1 atlas px ≈ 1 world tile), so the lone pixel stays crisp.

Update the UnitPass header comments that described the shell as 3×3.
2026-06-08 14:31:02 -07:00
evanpelle ea95069604 Move special effects toggle to graphics settings, wire to renderer
The special effects toggle wrote settings.specialEffects but nothing in
the WebGL pipeline read it — the FX pass is gated on passEnabled.fx. The
setting was orphaned when the old canvas renderer was removed, so the
toggle had no visual effect.

Move the toggle into the graphics settings modal (under a new Effects
section) and remove it from the in-game settings modal and the homepage
user settings modal. Rewire it to a passEnabled.fx graphics override so
it actually toggles the FX pass, applied live via the existing graphics
override listener.

Delete the now-dead fxLayer()/toggleFxLayer() from UserSettings.

Note: users who previously disabled special effects will reset to on,
since the old settings.specialEffects key is no longer read.
2026-06-08 14:15:07 -07:00
evanpelle 1c1728f6fa Add map hover/railroad graphics overrides and fix territory highlight
Extend GraphicsOverrides with a mapOverlay group (territory highlight,
border highlight amount, border highlight thickness) and a railroad
group (train track draw distance), wired through the schema,
applyGraphicsOverrides, and new sliders in the graphics settings modal.

Fix the territory hover highlight: the shader received uHighlightBrighten
but ignored it, applying a hardcoded saturation boost so the setting had
no effect. It now drives a contrast boost (push channels away from
mid-gray), with 0 disabling the effect.

The train track slider is presented as a "draw distance" (inverted
railMinZoom) so higher = tracks stay visible when more zoomed out.

Also move the Graphics settings button to the top of the settings modal.
2026-06-08 14:03:01 -07:00
tnhnblgl 7921261ac9 Countdown before game start (#4198)
Resolves #4178 

## Description:

Let's the timer countdown remaining time to start in spawn phase

<img width="343" height="82" alt="Screenshot_2026-06-06-11-24-26-193_com
android chrome"
src="https://github.com/user-attachments/assets/e5827db4-a6d5-485f-b504-d8b64b7c6ba7"
/>

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

Dovg
2026-06-08 12:58:18 -07:00
Katokoda 01ddc1d6c6 Fix aliance renewal question (extension-prompt) staying open (#4196)
> **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 #4164

## Description:

Single line change, making sure the ActionableEvent is updated after
removing the alliance that just ended.

## Please complete the following:

- [x] I have added screenshots for all UI updates
-# There are none
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
-# There are none
- [x] I have added relevant tests to the test directory
-# I do not know how I would to it.

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

Katokoda
2026-06-08 12:52:37 -07:00
Evan 26d8a314ae Scale defense-post border + fill rendering to thousands of posts (#4181)
## Description

Scales the defense-post border effect so it works with **thousands** of
Defense Posts instead of silently capping at 64.

### Problem
The border "checkerboard" (drawn on a player's border tiles when a
same-owner Defense Post is within range) was computed per-pixel: for
every border fragment, the shader looped over a `uniform vec4
uDefensePosts[64]` array doing a distance test. Two issues:
- **Hard cap of 64** — posts beyond the first 64 were dropped, so their
checkerboard never appeared.
- **Wrong cost shape** — work was `border_tiles × posts`; every added
post made every border pixel slower.

### Solution: invert the loop into a coverage texture
New `DefenseCoveragePass` stamps one instanced circle per post into a
map-resolution `R8` coverage texture (`1.0` = tile is within range of a
**same-owner** post; the owner check samples `tileTex` at stamp time, so
enemy posts never light up your border). It's a single
`drawArraysInstanced` regardless of post count — the same instancing
pattern `UnitPass`/`StructurePass` already use. The border-stamp shader
now reads one texel of that texture instead of looping; the old uniform
array, the 64-cap, and the per-fragment scan are removed from
`border-compute`/`BorderStampPass`/`BorderScatterPass`.

### Incremental re-stamping (dirty-block grid)
Coverage depends on tile ownership, which drips every frame during
combat, so a full re-stamp every frame would be wasteful at high post
counts. Because a tile changing owner only changes *its own* coverage,
the pass tracks a grid of dirty **blocks** and re-stamps only the blocks
containing changed tiles, scissored to each block (`gl.scissor` confines
the clear + draw to the changed region). Post add/remove and full tile
uploads fall back to a whole-map stamp; so does a frame where most
blocks are dirty. Per-frame cost tracks *how much changed*, not *how
many posts exist*, and scattered fronts (e.g. opposite corners) become
independent small block draws.

### Territory-fill darkening
The coverage texture marks every same-owner in-range tile (interior
included, not just borders), so `TerritoryPass` now also samples it to
darken the territory **fill** around posts. New tunable
`mapOverlay.territoryDefenseDarken` (live-editable in the graphics debug
GUI alongside `defenseCheckerDarken`).

### Performance
Tested with ~1,000 posts blanketing a map — smooth, including on a
low-end (~10-year-old) Chromebook.

## Files
- **New:** `passes/DefenseCoveragePass.ts`,
`shaders/defense-coverage/defense-coverage.{vert,frag}.glsl`
- **Edited:** `Renderer.ts`, `BorderStampPass.ts`,
`BorderComputePass.ts`, `BorderScatterPass.ts`, `TerritoryPass.ts`,
`border-stamp.frag.glsl`, `border-compute.frag.glsl`,
`territory.frag.glsl`, `RenderSettings.ts`, `render-settings.json`,
`debug/Layout.ts`

## Notes
- No user-facing text (no `translateText`/`en.json` changes needed).
- No `src/core` changes — purely client rendering, so no simulation
tests; verified via `tsc`, ESLint, `build-prod`, and in-game.
2026-06-08 10:18:02 -07:00
TKTK123456 8115b755a2 Fix rebinded keys (#4175)
> **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 #4174 

## Description:

I have fixed the rebinding of the keys for the build menu modifier and
the emoji menu modifier in the settings so that they actually work.
And cleaned up the code

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory

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

tktk1234567
2026-06-06 18:29:16 -07:00
evanpelle 385b4dd686 Add steady glow effect beneath hydrogen bomb
Render a soft radial glow underneath the hydrogen bomb sprite in
UnitPass. H-bomb instances draw an enlarged quad (hBombGlowScale) so
there's room for the halo; a cell-space UV remap keeps the sprite at its
normal size while the margin becomes glow area. The glow is a steady
(non-pulsing) radial falloff in a warm amber, alpha-blended underneath
the sprite and suppressed in alt/affiliation view.

Detection uses a HYDROGEN_BOMB_COL shader define derived from
UNIT_ORDER, so it tracks the atlas layout rather than hard-coding the
column. All other units are unaffected (scale 1, same fillrate); this
stays a single program / two instanced draw calls.

Glow color, scale, strength, and falloff are exposed in
render-settings.json for live tuning via the debug GUI.
2026-06-05 19:47:07 -07:00
Ryan 312b38fda5 Disable game buttons (clan tag + username) (#4170)
## Description:

disables buttons, instead of emitting a warning 
<img width="1017" height="677" alt="image"
src="https://github.com/user-attachments/assets/7af4e0e1-df22-4cfe-bc8b-6fae5e62f9b6"
/>


<img width="1006" height="668" alt="image"
src="https://github.com/user-attachments/assets/d8e5291c-4ecd-4f8d-8471-e5a547c30eda"
/>


## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory

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

w.o.n
2026-06-05 14:18:31 -07:00
Evan 075547b7b6 Incremental GPU scatter recompute for tile borders (#4166)
## Description:

Incremental GPU border recompute — sequel to #4159.

On 10 yo low-end chrome book this increased performance by ~5fps. I'm
now able to get 40fps on GWM.

`BorderComputePass` previously re-ran its fragment shader over every
tile on
the map every time any input changed (tile flip, highlight, relation,
defense post). Cost was O(mapW × mapH) per invalidation, and tile flips
invalidate it ~every render frame in live play.

This PR adds `BorderScatterPass`, which runs the same fragment shader
but
rasterizes only one POINT per dirty tile (plus its 4 cardinal neighbors,
to
cover the cardinal-neighbor read in the border shader). Cost is O(dirty
tiles) regardless of map size or spatial distribution.

### What changed

- New `BorderScatterPass` — owns its own FBO, VAO, and instance buffer;
  shares the border fragment shader with `BorderComputePass` so the two
  paths can't diverge in output.
- `BorderComputePass.draw()` now picks per frame:
- **Full recompute** — when `globalDirty` is set by highlight / relation
/
    defense-post changes (those affect tiles across the whole map).
  - **Scatter** — when only per-tile patches have been queued via
    `patchTile()`.
- `TerritoryPass.flushTileTexture()` now returns `"none" | "full" |
  "scatter"` instead of `boolean`, so the renderer can pick the right
  downstream invalidation:
- `"full"` → `borderPass.markGlobalDirty()` (full tile upload supersedes
    per-tile patches).
  - `"scatter"` → no-op; per-tile patches were already pushed via the
    wired `borderPatchConsumer` callback during drip drain.
- Renderer wires `territoryPass.setBorderPatchConsumer((x, y) =>
borderPass.patchTile(x, y))` so every per-tile scatter write to
`tileTex`
  also schedules an incremental border recompute for that tile + its
  neighbors.

### Known limitation

Highlight-thicken rings (within `uHighlightThicken` of a changed tile)
are
NOT incrementally repainted — they'll lag visually until the next full
recompute. In practice this is short-lived (the next highlight change or
seek triggers a full recompute) and not visible during normal play; the
trade is documented in the `BorderScatterPass` header.


## 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
2026-06-05 13:29:50 -07:00
Evan d1ce199a52 Upload tile delta to GPU (#4159)
## Description

Reduces the amount of tile data sent to the gpu each tick, roughly
~10fps rate increase on 10 year old chromebook.

Two changes to the territory rendering path:

### 1. Split `passEnabled.mapOverlay` into four flags

The single `mapOverlay` toggle controlled four unrelated passes
(territory fill, border compute, border stamp, trail). Splits it into
`territory`, `borderCompute`, `borderStamp`, `trail` so each can be
toggled independently in the debug GUI. Pure rename — default behavior
is unchanged (all four default to `true`).

### 2. GPU scatter for per-frame tile texture updates

Replaces the dirty-row bbox `texSubImage2D` upload in `TerritoryPass`
with a new `TileScatterPass` that uploads a small attribute buffer of
`(x, y, state)` patches and runs a single `POINTS` draw into an FBO
bound to `tileTex`. Each patch rasterizes as a 1×1 point into exactly
its target texel.

**Why:** the old path's cost scaled with the bounding box of the dirty
rows, not the number of changed tiles. In typical play, tile changes are
spread across the whole map (multiple players fighting in different
regions, scattered trails/fallout), so the bbox covered most of the
map's rows and we re-uploaded mostly-unchanged data every frame. The new
path is constant cost in patch count regardless of spatial distribution,
and no longer scales with map size.

The full-upload path (initial load / seek / spawn-phase flush) is
unchanged. `fullUploadPending` correctly supersedes any queued scatter
patches.

## Please complete the following:

- [x] I have added screenshots for all UI updates *(N/A — no UI
changes)*
- [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 *(renderer code,
not covered by unit tests; verified visually)*

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

evan
2026-06-05 07:07:03 -07:00
Evan 2c2390d0cb Downsample fallout bloom + light extract for fillrate-bound GPUs (#4157)
## Description:

On low-end machines, the fillrate was too high causing framerate to
drop. The graphical difference is pretty negligible since fallout &
light are meant to be blurred anyways.

Reduces fillrate cost of the fallout bloom and fallout-light passes on
low-end GPUs:

- Extract step now renders at `mapW/8 × mapH/8` (64× fewer fragments).
Output is heavily blurred + LINEAR-magnified, so the visual difference
is minimal.
- Bloom blur reduced from 2× 9-tap to 1× 5-tap Gaussian (the smaller
kernel is sufficient given the lower-res source).


## 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
2026-06-04 16:53:03 -07:00
evanpelle 986f0b61bf Fix WorldTextPass labels scaling with device-pixel-ratio
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.
2026-06-03 21:40:00 -07:00
evanpelle 96c032850d Fix start game button regression in lobby modals
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.
2026-06-03 20:51:28 -07:00
evanpelle 88697955b7 Add SoundEffectController to restore v31 sound effects
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.
2026-06-03 20:43:12 -07:00
Ryan 9c2ac05506 clantag part 1 (#4066)
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
2026-06-03 14:25:55 -07:00
Evan 48609fa70a Reduce lobby broadcast bandwidth via counts-only deltas (#4116)
## 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
2026-06-02 15:52:14 -07:00
evanpelle 431f22ac94 Always render player name when under the cursor 2026-06-02 12:04:05 -07:00
Evan f1045a2022 Update & refactor dark mode (#4114)
## 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
2026-06-02 11:48:52 -07:00
Aotumuri 2386b4b38a Restore dev-only localStorage pattern override (#3999)
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
2026-06-02 09:50:27 -07:00
noahschmal 2c8a66625c Feature/Move theme system from core to client-side ThemeProvider (#4108)
**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>
2026-06-02 09:32:08 +00:00
TKTK123456 6ca06a6f6f Sam/factory radius ghost upgrade fix (#4104)
> **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
2026-06-01 17:39:52 -07:00
TKTK123456 c722b026db Sync factory effective distance and railroad max length and add railroad ghost for factories (#4079)
> **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
2026-06-01 17:39:26 -07:00
evanpelle 82bfae981f Allow shallow ocean tiles in name box grid for larger island player names 2026-06-01 11:22:34 -07:00
a-happy-goose 8dfa2b4cd9 [small-fix 25 lines] Add boat ETA calculation and display in AttacksDisplay (#4097)
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
2026-05-31 20:34:16 -07:00
Evan 712b2bc473 Show bonus amount on currency packs (#3907)
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
2026-05-31 15:09:36 +01:00
evanpelle 450f2944c9 don't show clan tab on crazy games 2026-05-31 15:09:08 +01:00
evanpelle 475a7ab8af bugfix: port construction bar completes early; renderer now reads durations from Config
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.
2026-05-29 12:15:49 -07:00
evanpelle 10bf2be102 Remove unused Structure Sprites setting
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.
2026-05-28 14:54:43 -07:00
evanpelle fc3d80ec73 Add Classic Icons toggle to Graphics Settings
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.
2026-05-28 14:47:40 -07:00
evanpelle e938e5936b Add Graphics Settings name color toggle and unit tests
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.
2026-05-28 13:54:05 -07:00
evanpelle 4cee61c7d1 fix(render): keep build-cost chip a fixed screen size
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.
2026-05-28 13:19:22 -07:00
Evan 20bc311caf Add Graphics Settings modal with live name-label tuning (#4065)
## 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
2026-05-28 13:06:43 -07:00
evanpelle 8142bc1070 Add Render Debug GUI toggle to settings modal
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.
2026-05-28 09:09:44 -07:00
evanpelle a5e28d81d2 fix(render): turn nuke range circle red when launch would break alliance
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.
2026-05-27 16:25:34 -07:00
evanpelle 21e42ce461 fix(render): only fire conquest FX for the local player's conquests
Skip ConquestEvent updates whose conquerorId isn't the local player so the
gold popup + sword sprite only triggers on your own captures.
2026-05-27 16:07:38 -07:00
evanpelle 9bf6b5af74 fix(render): only show transport attack rings for the local player's boats
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.
2026-05-27 15:54:17 -07:00
evanpelle 23fbc3114a feat(render): fade railroad overlay near min zoom
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.
2026-05-27 15:42:11 -07:00
Evan aa3959bffe feat: territory png based skins (#4006)
## 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
2026-05-27 13:00:07 -07:00
Berk c35355a490 fix(client): silence noisy LangSelector not found console warning (#4021)
## 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
2026-05-26 19:12:00 -07:00
Berk 10776c1948 fix(render): prevent trade-friendly ships from visually rendering as angry red warships (#3843) (#4017)
## 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
2026-05-26 19:10:34 -07:00
evanpelle 2b45813ce0 Use spawn tile for name placement during spawn phase
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.
2026-05-24 17:18:11 +01:00
Evan b4a14f9b9d Move attack troop overlay to WebGL (#3996)
## 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
2026-05-24 16:47:34 +01:00
Aotumuri cbf38ffb81 Fix destroyed cooldown structures reappearing in game (#3997)
## 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
2026-05-24 16:05:29 +01:00