Part of #4040 (v1 scope: listing + browser + per-subscriber limit;
custom lobby name/description left for a follow-up).
## What
Subscribers can toggle their **private lobby** to be **publicly
listed**; a browsable **"Open Lobbies"** list appears in the Join Lobby
modal. Hard limit of **one listed lobby per subscriber**, enforced
cluster-wide.
## How
**Semantics** — a listed lobby stays `GameType.Private`: the host keeps
full control and starts the game manually; the toggle only controls
visibility. The `listed` flag lives on `GameServer` (not `GameConfig`),
so it cannot be smuggled in through `update_game_config` and never
touches core/sim/records.
**Distribution** — reuses the existing public-lobby pipeline end to end:
a new `"hosted"` `PublicGameType` bucket flows worker → master IPC →
`/lobbies` websocket → `PublicLobbySocket`. Master scheduling now
iterates only `SCHEDULED_PUBLIC_GAME_TYPES` (`ffa`/`team`/`special`), so
it never sets countdowns on or schedules replacements for hosted
lobbies. Lobbies delist automatically when the game starts/fills/dies
(phase change). The broadcast fingerprint now includes browser-visible
config, so host edits (map/mode) refresh the list even though the gameID
doesn't change.
**Gating** — new authenticated endpoint `POST /api/game/:id/listing`:
- creator-only (403), private + not-started only (409)
- fresh subscription check via server-side `getUserMe` using the shared
`hasActiveSubscription()` helper (`active`/`trialing`); skipped in
`GameEnv.Dev` (same precedent as Turnstile) so it's testable locally
- one-lobby-per-creator (409): a SHA-256 hash of the creator's
persistentID rides worker↔master IPC (`PublicGameInfo.creatorID`); the
master dedupes as a race backstop. The hash — and host-only config
(whitelist, name reveals) — are **stripped from every client payload**
(broadcast + primed snapshot).
**Client** — subscriber-gated "List lobby publicly" toggle in the host
modal (server rejection reverts the toggle and shows a translated
message); "Open Lobbies" rows (map, mode, player count) in the Join
Lobby modal that reuse the existing private-join flow.
**Compat** — `PublicGames.games` is now a `partialRecord`, so newer
clients tolerate servers that don't send every bucket. Note:
already-open old clients will fail to parse broadcasts containing the
new `hosted` key until refreshed (closed Zod enum) — same class of break
as previous wire-schema changes.
## Testing
- `tests/server/HostedLobbyListing.test.ts` (15 tests): listed-lobby
filtering, flag not settable via config intent, master aggregation +
creator dedupe + no scheduling of hosted, creatorID stripping (broadcast
+ primed snapshot), `creatorHasListedLobby` (broadcast + local),
fingerprint refresh on config change
- `hasActiveSubscription` cases in `ApiSchemas.test.ts`; hosted
counts-delta patch in `LobbySocket.test.ts`
- Full suite green (1723 + 141 tests), tsc/eslint/prettier clean
- **E2E in the real app** (headless Chromium, two browser contexts):
host lists lobby → appears in second browser's Join Lobby list
(creatorID absent from payload) → join succeeds (2 players in lobby) →
same creator's second lobby rejected 409 with toggle revert → unlist
removes it from a fresh browser's list. Curl negatives: missing auth
400, bad token 401, non-creator 403, missing game 404, bad body 400.
## Known follow-ups
- Custom lobby name/description in the browser (needs the censor
pipeline) — rest of #4040
- A listed lobby whose host closes the tab stays advertised indefinitely
(an empty private lobby never leaves the Lobby phase) — pre-existing
lifecycle, now more visible; consider delisting on creator disconnect
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Resolves Issue #4463
## Description:
An optional game mode that (almost) guarantees a finish instead of
letting late-game
stalemates drag on.
Originally called sudden death, renamed to Doomsday clock
Once enabled, every side (each player in FFA, each whole team in team
modes)
must hold a rising share of the map. A side below the bar is skulled;
after a
short warn its troops bleed to zero, forcing consolidation to a winner.
### How it works
- **Rising zone:** a grace period, then the required share ramps up
linearly to
each level with 30s pauses between (a battle-royale "zone"). Levels
track the
ofstats FFA territory median (3/5/10/20/30%).
- **Four speed presets** (slow / normal / fast / very fast) change only
the pace:
normal ends ~30 min, very fast ~15.
- **Troop decay:** a linear ramp as a % of max capacity, ~50s from
caught to zero
(10s warn + ~50s ≈ 1 min total).
- **UI:** a HUD panel (live share vs target, wave/decay countdowns,
red/orange
cues) and an on-map skull above flagged players (blinks in danger,
steady while
draining).
### Notes for review
- Off by default; no effect on existing games. However, as discussed we
can add it to the modifier pool for public games to see how popular the
gamemode is vs normal play.
- Sim is deterministic (integer-only, in `src/core`), covered by unit +
integration tests.
- One-line addition to `GameServer.updateGameConfig` so the setting
survives the
host → server → client round-trip.
- Status is packed into the existing name-pass data slot (`pd4.w`: 0/1/2
=
none/danger/draining); the skull is composited into the icon atlas at
load.
### Testing
`npm test`, `npm run lint`, `npx prettier --check .`, `npm run
build-prod` all pass.
### UI:
<img width="243" height="100" alt="Image"
src="https://github.com/user-attachments/assets/c4c9eeb0-4feb-437d-9aac-b2786a841b74"
/>
Dropdown between slow, normal, fast, very fast
Before zone:
<img width="302" height="175" alt="Image"
src="https://github.com/user-attachments/assets/7359a1ea-4951-446d-a23c-0711fe06cc5d"
/>
Zone started, player not affected the pannel also blinks orange for 10s:
<img width="297" height="175" alt="Image"
src="https://github.com/user-attachments/assets/fcc565a5-d5d0-47a7-97ea-d0ba9d9ad899"
/>
Player affected, grace period (Danger):
<img width="314" height="170" alt="Image"
src="https://github.com/user-attachments/assets/ff96d21e-96f3-4ef9-8190-48eecc7aac0f"
/>
Skull icon blinking over player (everyone sees it) - older screenshot,
the clipping has been fixed
<img width="462" height="145" alt="Image"
src="https://github.com/user-attachments/assets/53899211-33b1-40e1-83f2-77f2096f0cad"
/>
Player affected, grace period ended (Draining):
<img width="360" height="159" alt="Image"
src="https://github.com/user-attachments/assets/4b226d57-da4d-4866-ab5f-db48e4ed1ea2"
/>
Skull icon no longer blinking, everyone can see you are in a state of
decay, and troops are draining:
<img width="732" height="146" alt="image"
src="https://github.com/user-attachments/assets/cd10fedb-6e87-4dfc-9fbf-55d3945a7901"
/>
Skull is visible like alliances icon also on player tab
<img width="558" height="81" alt="Image"
src="https://github.com/user-attachments/assets/6acdbe91-bdd0-40c7-942b-3990d4dae87f"
/>
(just UI example, best way to see it is to hop on a solo game and play
against AI)
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
## Please put your Discord username so you can be contacted if a bug or
regression is found:
zixer._
> **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#4414
## Description:
The 2 lines added check whether it's a replay or a ranked game. If so,
don't show the FFA collusion warning
## Please complete the following:
- [X] I have added screenshots for all UI updates (couldn't sign in via
the dev server)
- [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:
lents (not in the development server yet, sent request to join)
## Description:
Add terrain color settings for all terrain types
<img width="977" height="485" alt="image"
src="https://github.com/user-attachments/assets/ac1cef11-4b1a-45f2-8cf6-94f557ba8f6e"
/>
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] I have added relevant tests to the test directory
## Please put your Discord username so you can be contacted if a bug or
regression is found:
MR. Box
**Add approved & assigned issue number here:**
Resolves#4316
## Description:
At the beginning of the leaderboard update cycle, evaluates `maxTroops`
once for each `PlayerView` and uses the cached value for the rest of the
`maxTroops` lookups in the function.
Measured a reduction in `updateLeaderboard` processing time from 6ms/sec
down to 2ms/sec (measured over the first minute of a singleplayer game
on world map and default settings).
## 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:
Demonessica
Resolves#4307
## Description:
Adds a check for the "delete" element of the raidial menu, to make sure
the unit attemped to be removed is a structure.
Previously, non-structure units such as trains could be deleted using
the menu.
## Please complete the following:
- [ ] I have added screenshots for all UI updates (No UI updates)
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file (No text added)
- [ ] I have added relevant tests to the test directory (Not sure how
this works or if it's needed)
## Please put your Discord username so you can be contacted if a bug or
regression is found:
unne27
---------
Co-authored-by: unne27 <uno.gunnar.johansson@gmail.com>
## What
Adds an on/off **Structure dots** toggle to the graphics settings modal
(Structure Icons section), controlling whether structures collapse into
small dots when zoomed out.
## How
The renderer already gates the dots LOD on `structure.dotsZoomThreshold`
(structures become dots when `zoom <= threshold`). The debug GUI exposes
that threshold as a slider; this surfaces a simple player-facing toggle:
- `GraphicsOverrides.ts` — adds `structure.showDots` (boolean) to the
override schema.
- `RenderOverrides.ts` — when `showDots === false`,
`applyGraphicsOverrides` sets `dotsZoomThreshold = 0`. Since zoom is
always > 0, the dots LOD never triggers, so structures keep their full
icon at every zoom. When enabled (default), the threshold is left
untouched.
- `GraphicsSettingsModal.ts` — toggle button mirroring the existing
Classic icons / Classic level numbers toggles; defaults to On.
- `en.json` — `structure_dots_label` / `structure_dots_desc`.
The change applies live: a `settings.graphics` change re-runs
`applyGraphicsOverrides` onto the live settings object the passes read
each frame, and `dotsZoomThreshold` is a per-frame uniform.
## Testing
- `tsc --noEmit` clean.
- Verified in a headless solo game: the toggle renders (default On),
flipping it persists `structure.showDots = false`, and
`applyGraphicsOverrides` yields `dotsZoomThreshold` 1.2 when on / 0 when
off.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Problem
The important-events notification panel splits events into a prominent
**tier-1** list and a dim, auto-expiring **tier-2** list (capped at the
4 newest entries). Two alliance notifications were landing in tier-2,
making them easy to miss:
- **"X wants to renew your alliance"** (`RENEW_ALLIANCE`) — the core
simulation still sends this when another player requests a renewal, but
it was buried in tier-2, so you couldn't reliably tell whether someone
had asked to renew.
- **"X rejected your alliance"** (`ALLIANCE_REJECTED`) — also tier-2, so
the outcome of a sent request was inconsistent (accepted was prominent,
rejected was not).
## Fix
Add `MessageType.RENEW_ALLIANCE` and `MessageType.ALLIANCE_REJECTED` to
`TIER_1_TYPES` in `EventsDisplay.ts`, so they show prominently alongside
the already-tier-1 `ALLIANCE_ACCEPTED` event.
No core/simulation changes — the messages were already being emitted;
only the client presentation needed fixing. Display styling for both
message types already exists in `Utils.ts`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
<img width="1152" height="866" alt="settings"
src="https://github.com/user-attachments/assets/dff00f2e-775d-4b7e-8590-855813dcff7d"
/>
## Description:
Small qol change to be able to preview graphics settings 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
Mr. Box
## What
Adds a **Nuke fallout color** option to the in-game graphics settings
modal (Effects section), letting players recolor the fallout tint left
on territory after a nuke.

## How
Mirrors the existing **Ocean color** override pattern:
- `GraphicsOverrides.ts` — adds `staleNukeColor` (hex string) to the
`mapOverlay` override schema.
- `RenderOverrides.ts` — `applyGraphicsOverrides` parses the hex and
writes the renderer's `staleNukeR/G/B` 0–1 float channels (`hexToRgb`
yields 0–255, so it divides by 255).
- `GraphicsSettingsModal.ts` — new hex-text + native color-picker row,
default computed from `render-settings.json`.
- `en.json` — `nuke_color_label` / `nuke_color_desc`.
The value persists via `UserSettings.graphicsOverrides()` and is cleared
by the modal's existing "Reset to defaults".
The render debug GUI already exposes the same setting as **Stale Nuke
Color** (Map Overlay), so no change was needed there.
## Testing
- `tsc --noEmit` clean.
- Verified in a headless solo game: the row renders with the green
default (`#0d8c12`), changing it persists `mapOverlay.staleNukeColor`,
and `applyGraphicsOverrides("#ff0000")` produces `staleNukeR=1, G=0,
B=0`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## What
Cap the height of the **Tier 1 (important) events panel** and make it
scroll when many events stack up, instead of letting it grow unbounded
up the screen.
## Why
The less-important (Tier 2) events panel was already height-capped and
scrollable, but the important panel had no limit — a burst of important
events (chat, nukes inbound, alliance changes, etc.) could push the
panel arbitrarily tall.
## Changes (`src/client/hud/layers/EventsDisplay.ts`)
- Added `max-h-[30vh] lg:max-h-[40vh] overflow-y-auto` to the
important-events container.
- Mirrored the existing Tier 2 auto-scroll-to-bottom behavior for the
important panel (new `.important-events-container` query + scroll
tracking), so the newest important events stay in view rather than being
hidden below the fold. If the player scrolls up, auto-scroll pauses
(same as Tier 2).
## Testing
Verified in the live game by injecting 15 important events:
- Panel is height-capped and scrollable (`scrollHeight 540 >
clientHeight 400`).
- Auto-scrolls to the newest (`scrollTop` pinned to bottom); events 5–15
visible, older ones reachable by scrolling up.
lint clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Problem
The troop and population ratio bars in `ControlPanel` and
`PlayerInfoOverlay` update their inline `width` on every game tick, with
a `transition-[width] duration-200` to smooth the change. But `width` is
a layout property — animating it forces the browser to **recalculate
layout for the surrounding HUD components every animation frame**. Since
the width changes every tick, this kept the whole HUD in a near-constant
relayout loop and showed up as jank.
## Fix
Keep the smooth animation, but drive it with `transform`
(GPU-composited, no layout) instead of `width`:
- Replace the two flex `width: %` segments with absolutely-positioned,
full-width bars.
- Segment 1: `transform: scaleX(green/100)` anchored to the left edge
(`origin-left`).
- Segment 2: `transform: translateX(green%) scaleX(orange/100)` so it
stays flush against the first segment.
- Animate with `transition-transform duration-200 ease-out`.
Because `transform` is composited rather than laid out, the bars animate
smoothly **without** triggering the per-frame HUD relayout.
The segments are now always mounted (`scaleX(0)` when empty) instead of
conditionally rendered, which also prevents the transition from
resetting as values cross zero.
Files:
- `src/client/hud/layers/ControlPanel.ts` (mobile + desktop troop bars:
malibu-blue / aquarius)
- `src/client/hud/layers/PlayerInfoOverlay.ts` (sky-700 / malibu-blue)
A grep confirmed these were the only `transition-[width]` usages in the
client.
## Testing
- `eslint --fix` / prettier ran clean via the pre-commit hook.
- CSS-only change; no sim/behavioral logic touched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Adds a **"Fallout effects"** toggle to the *Effects* section of the
graphics settings modal, letting players disable the nuclear fallout
visuals (useful for performance).
Fallout is rendered by two passes — the broiling green **bloom** on
irradiated territory and its additive **light** contribution in
day/night mode. The bloom pass was already gated by
`passEnabled.falloutBloom`, but the light pass had no gate. This adds a
`passEnabled.falloutLight` flag and a single user-facing
`passEnabled.fallout` graphics override that drives both together.
## Changes
- **`RenderSettings.ts` / `render-settings.json`** — new
`passEnabled.falloutLight` flag (default `true`).
- **`LightmapPass.ts`** — gate the fallout light pass behind
`passEnabled.falloutLight`.
- **`GraphicsOverrides.ts`** — add `fallout: z.boolean()` to the
`passEnabled` override group.
- **`RenderOverrides.ts`** — apply `passEnabled.fallout` to both
`falloutBloom` and `falloutLight`.
- **`GraphicsSettingsModal.ts`** — `currentFallout()` /
`onToggleFallout()` + a toggle button (mirrors the existing Special
Effects toggle).
- **`en.json`** — `graphics_setting.fallout_label` / `fallout_desc`.
## Testing
- `tsc --noEmit` passes; JSON files validated.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## What
Removes the binary **dark mode** feature and replaces it with a
player-adjustable **Lighting** section in graphics settings.
### In-game settings
- Removed the Dark Mode toggle from both `SettingsModal` and
`UserSettingModal`, and `darkMode()`/`toggleDarkMode()`/`DARK_MODE_KEY`
from `UserSettings`.
### New Lighting section (Graphics Settings)
- **Ambient light** slider (1–3): mapped to the renderer's ambient as
`ambient = 1 / level`. **1.0 = no effect (unchanged look), 3.0 = darkest
with the strongest structure glow.**
- **Light falloff** slider (1–3): writes straight to
`lighting.falloffPower`.
- Lighting auto-enables only when ambient < 1, so the default (slider at
1) has zero GPU cost — off by default.
### Removed dark-mode overrides
- Deleted `applyDarkModeOverride()` + `DARK_AMBIENT` and their wiring in
`ClientGameRunner`, `gl/index.ts`, and the `DARK_MODE_KEY` listener.
- Removed the `.dark` HUD-class toggle in `Main.ts` and the
`userSettings.darkMode()` read in `PlayerIcons`.
### Train glow
- `UT_TRAIN` light reduced (intensity `2.0 → 0.5`, radius `8 → 6`) so
structures dominate the glow.
## Notes
- Removing the dark-mode setting also retires the HUD's Tailwind dark
theme (same setting). The dormant `dark:` CSS variants and unused
white-icon assets are left in place (out of scope).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## What
Adds a **Terrain** section to the graphics settings modal with a color
picker and a hex-code text field (paste a `#rrggbb` code) for the
**ocean** (deep water) color.
## Details
- The picked color sets the *shallow-water base*; the existing per-depth
brightness gradient is preserved (deeper water still darkens).
- Only deep water is affected — shoreline water and land are untouched.
- Follows the same override pattern as every other graphics setting: the
default lives in `render-settings.json` (`terrain.oceanColor`), the
override is a field in `GraphicsOverrides`, and `applyGraphicsOverrides`
copies it into the live `RenderSettings`.
- Rebased on #4271 (settings resolved before renderer construction): the
terrain texture **bakes the resolved ocean color at construction**, so a
saved override shows on load with no special-casing. Terrain is baked
into a GPU texture rather than read per-frame, so a *live* change still
triggers an explicit `view.rebuildTerrain()`.
- Resetting graphics overrides clears it back to the default ocean
color.
## Testing
Verified live in a headless singleplayer game:
- A **saved** ocean override renders green deep-water on load, baked at
construction with no settings-change event fired.
- A mid-game color change recolors the deep ocean instantly, gradient
preserved, shoreline/land untouched.
`tsc` and ESLint clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
Adds a new **Structure icon size** option to `GraphicsOverrides`,
exposed as a slider in the Graphics Settings modal. Players can now
scale how large structure icons are drawn on the map.
## Changes
- **`GraphicsOverrides.ts`** — add `iconSize: z.number()` to the
`structure` override schema.
- **`RenderOverrides.ts`** — apply the override onto
`settings.structure.iconSize` (consumed by
`StructurePass`/`StructureLevelPass` shaders).
- **`GraphicsSettingsModal.ts`** — add a slider (range 20–120, step 5)
in the "Structure Icons" section, with getter/handler following the
existing pattern. Falls back to the `render-settings.json` default of 60
when unset.
- **`resources/lang/en.json`** — add `icon_size_label` /
`icon_size_desc` (English only, per i18n rules).
- **`tests/GraphicsOverrides.test.ts`** — schema-validation cases plus
application tests (override sets the value; absence keeps the default).
The setting persists via the existing `userSettings.graphicsOverrides()`
localStorage flow and takes effect live through the existing
`regenerateRenderSettings` wiring.
## Testing
- `npx vitest tests/GraphicsOverrides.test.ts --run` — 35 passed
- `tsc --noEmit` — no new type errors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## What
Structure **level numbers** now render in the **`round_6x6_modified`**
bitmap font by default (matching the old PIXI-based `StructureLayer` /
`v31`), with a graphics setting to switch back to the smooth
`overpass-bold` MSDF font.
Two commits:
1. **Default to the classic bitmap font** — `StructureLevelPass` drew
level digits from the `overpass-bold` MSDF atlas (the one `NamePass`
uses for player names); switch the default to the `round_6x6_modified`
pixel font (white digits with a baked-in dark outline).
2. **Add a runtime toggle** — load both fonts and switch between them
live via a new `Classic level numbers` graphics setting.
## How
- `StructureLevelPass` loads both atlases up front and selects one per
frame from `settings.structureLevel.classicFont`, re-laying-out the
digits when the toggle flips (digit advances differ between the fonts).
The fragment shader is a single program with a `uClassic` branch: direct
bitmap sample (white fill + baked outline) vs. MSDF median + synthesized
outline.
- New override `structure.classicNumbers` in `GraphicsOverrides`
(default `true` = classic), applied onto
`settings.structureLevel.classicFont` in `applyGraphicsOverrides` — so
it switches live, like the existing colorblind/classic-icons toggles.
- `GraphicsSettingsModal` gets a `Classic level numbers` toggle next to
`Classic icons` (with `en.json` strings).
## Testing
- `tsc --noEmit`, ESLint, Prettier, and `npm run build-prod` all pass.
- Ran the game headless, built/upgraded cities to level 2–3, and
confirmed: the classic toggle renders the pixel font, flipping it
renders the smooth MSDF font, and flipping back restores the pixel font
— switching live with no shader errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bring the WebGL structure renderer closer to the old canvas render:
larger icons, classic icon styling by default, and more prominent,
better-positioned level numbers.
Troops are stored internally at 10x their displayed value, but the
donation event message formatted the raw amount with renderNumber
instead of renderTroops. Gold is unscaled and was already correct.
## Summary
The WebGL renderer was adapted from an external extension and carried a
lot of machinery this integration never uses (replay playback, its own
input/event system, a GL radial menu). This PR is two mechanical cleanup
passes with **no behavior change**: delete the dead code, then untangle
the `GameView` naming collision.
**78 files, +142 / −2,197.**
### Pass 1 — remove dead extension baggage
- **Replay/copy mode**: `FrameData.tileMode` was hard-coded `"live"`;
the copy branches in `frame/Upload.ts`, `UploadOptions` (never passed),
`applyFullFrame`/`applyFullTiles`/`applyDelta` on the facade and
`GPURenderer`, `HeatManager.resetForSeek`, and the seek-upload methods
on `TerritoryPass`/`TrailPass` were all unreachable. Also deletes
`types/Replay.ts`, `types/FrameSource.ts`, `types/GameUpdates.ts`,
`types/Game.ts` (imported only by the types barrel).
- **FrameEvents**: trimmed from 14 fields to the 3 actually populated
and read (`deadUnits`, `conquestEvents`, `bonusEvents`). The other 11
fed the extension's stats system and were never written or read here.
- **GL radial menu**: `RadialMenuPass`, its 4 shaders, and ~10 API
methods on facade + renderer had zero callers — the game uses the DOM/d3
radial menu in `hud/layers/RadialMenu.ts`. The pass was constructed and
drawn every frame for nothing.
- **Facade event system**: `GameViewEventMap` defined 10 event types
(`click`, `hover`, `scroll`, …) but only `contextrestored` was ever
emitted — input actually flows through `InputHandler` → EventBus →
controllers. Replaced the listener map with a single `onContextRestored`
callback and deleted `Events.ts`. Also fixed the stale header comment
claiming the facade handles user interaction.
- **Unused API surface**: removed ~20 facade/renderer methods with zero
callers (camera passthroughs like
`panTo`/`zoomTo`/`fitMap`/`screenToWorld`, hit-testing queries, SAM
replay setters, `setSelectedUnit`, `clearFx`/`setFxTimeFn`,
`onFrame`/`afterRender`/fps tracking).
Deliberately left alone: `Camera`'s pan/zoom primitives (building blocks
for a possible future camera unification) and the `timeFn` plumbing
inside the FX passes (deeply embedded as defaults; only the dead
renderer-level wrappers were removed).
### Pass 2 — untangle the three GameViews
- `render/gl/GameView.ts` → **`MapRenderer.ts`** (class `MapRenderer`).
Every importer was already aliasing it as `WebGLGameView` to dodge the
collision with the simulation-mirror `GameView` in `client/view/`, so
this removes aliasing rather than adding churn. `render/CLAUDE.md`
updated.
- Deleted the `src/core/game/GameView.ts` back-compat shim (its own TODO
asked for this). All 51 importers now import from `src/client/view/`
directly via a new 3-line barrel `view/index.ts`.
## Test plan
- `tsc --noEmit` clean, `eslint` clean
- Full test suite passes (1,385 + 65 server tests)
- Manual verification via headless Chromium: started a singleplayer game
and confirmed the renderer works end-to-end — terrain draws, spawn-phase
overlay shows, territories fill with borders after spawning, player
names/flags render, no renderer console errors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Resolves#4220
## Description:
Filters `ActionableEvents` to remove dead requestors.
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] I have added relevant tests to the test directory
**I do not know how** to create and then kill a player in
`tests/client/graphics/layers/ActionableEventsAlliance.test.ts`.
## Please put your Discord username so you can be contacted if a bug or
regression is found:
Katokoda
**Add approved & assigned issue number here:**
Resolves#3839
## Description:
A bunch of small updates to make the coordinate grid a lot nicer.
- Removes black backgrounds on text.
- Allows user to modify the opacity of the coordinate grid
- Persist the enable state of the coordinate grid
### Before
<img width="2344" height="1168" alt="image"
src="https://github.com/user-attachments/assets/22c2fb77-9db6-41bf-a50a-987f651cc19a"
/>
### After
<img width="2331" height="1174" alt="image"
src="https://github.com/user-attachments/assets/0e5a9575-8a79-407b-8d78-8564df02b259"
/>
<img width="407" height="947" alt="image"
src="https://github.com/user-attachments/assets/b9e5f9f1-3cc1-4832-b7d4-38e1f5e93d57"
/>
## 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
## Please put your Discord username so you can be contacted if a bug or
regression is found:
FrederikJA
The hovered player (tile owner under the cursor, already tracked via
uHighlightOwnerID for cull bypass) now gets a soft white glow behind
their name. The glow is derived from the MSDF distance field: a white
band past the outline with quadratic falloff, composited behind the
glyph and clamped to the SDF margin so it never clips at quad edges.
Glow size and strength are tunable via hoverGlowWidth/hoverGlowAlpha
in render-settings.json, exposed as sliders in the graphics settings
modal (persisted as graphics overrides) and in the debug GUI.
Includes schema and apply tests for the new override fields,
covering the 0 edge case (0 disables the glow, not "unset").
Resolves#3900
## Description:
During the spawn phase in FFA games, display a collusion warning to
clearly communicate to new users that pre-game agreement is not allowed.
<img width="1362" height="662" alt="2026-06-01_20-31"
src="https://github.com/user-attachments/assets/bd083e91-280a-41e1-a11a-d69d5f16bc8a"
/>
- [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
---------
Co-authored-by: evanpelle <evanpelle@gmail.com>
**Add approved & assigned issue number here:**
Resolves#2549
## Description:
Adds colorblind mode. Similar to dark mode, it exists as a toggle in
settings. When enabled, it swaps the game's theme (which is refactored
to extend from a theme base class) to use more colorblind-friendly
colors and brightness variations. Borders are darkened, and terrarin is
separated by lightness. Friendly/Foe colors and switched to blue/orange
instead of red/green.
The theme refactor supports adding new themes without having to
reimplement the color distribution system. New themes can extend the
BaseTheme and supply the data, such as palettes, team-color variations,
and terrain.
New setting:
<img width="880" height="273" alt="Screenshot 2026-06-04 at 11 30 27 AM"
src="https://github.com/user-attachments/assets/d5d573d5-cc64-4ac1-95c2-00627faf17cc"
/>
New color palette:
<img width="1119" height="757" alt="Screenshot 2026-06-04 at 11 30
59 AM"
src="https://github.com/user-attachments/assets/2bb15bc9-992b-41ae-ab0e-b01fe0c3c6bb"
/>
## 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
## Description:
Player name plates can block the view of what's underneath them
(structures, units, terrain). This PR fades the entire name plate —
name, troop count, flag, and emoji/status row — to 25% opacity while the
cursor is over it, so you can see and click what's behind it.
**How it works:**
- `HoverHighlightController` pushes the cursor's world position into the
renderer on mouse move.
- `NamePass` hit-tests the cursor against each player's name plate
bounds on the CPU (mirroring the lerp/sizing math in `name.vert.glsl`)
and passes the matched player's ID to the text, icon, and status-icon
programs, which apply the alpha multiplier in their shaders.
**Graphics setting:**
- New "Name opacity under cursor" slider in the Graphics Settings modal
(Name Labels section), range 0–1, default 0.25. Setting it to 1 disables
the fade entirely.
- Wired through the existing `GraphicsOverrides` pipeline: changes apply
live and are cleared by "Reset to defaults".
- Tuning knob exposed as `name.hoverFadeAlpha` in `render-settings.json`
and the debug GUI.
## Please complete the following:
- [ ] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] 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
Local-player rails previously rendered in the white focused-border color
from the palette, making them hard to see on light territory. Rails now
use a dedicated local rail color: white normally, flipped to black when
the territory backdrop is too light for white to read against (patterns
average their primary/secondary brightness).
Also add a railThickness render setting (0.5-3, default 1), exposed in
the Graphics Settings modal and the debug GUI, and persisted via
GraphicsOverrides. In the medium-zoom LOD, rails are now drawn as
screen-space anti-aliased lines around each tile's rail centerline,
accumulated from the 3x3 neighborhood so thick lines spill cleanly into
neighboring tiles; detailed mode scales its sub-grid band widths.
- PlayerView: compute railColor() (white/black by backdrop brightness)
- RailroadPass/shader: uLocalPlayerID, uLocalRailColor, uRailThickness
- render-settings.json, RenderSettings, GraphicsOverrides,
RenderOverrides: new railroad.railThickness knob
- GraphicsSettingsModal: "Train track thickness" slider (+ en.json keys)
- tests: schema + apply coverage for railroad overrides
Resolves https://github.com/openfrontio/OpenFrontIO/issues/3445
## Description:
I copied the PR #3743 from @luctrate (Add army limit warning indicator
for team games) to this PR because he didn't respond to requested
changes but I thought it's important.
I expanded on it, now its a full help message system:
**Warnings (orange):**
- Army limit: shown in team games with donations when troops exceed 80%
of max
- Low troops: shown when troops drop below 1k (=> new noob player who
clicks too much)
<img width="764" height="251"
alt="582494157-cf19b13e-a0a9-44e4-8de8-86c007fe9c79"
src="https://github.com/user-attachments/assets/6b4996d9-1993-4d2c-98ba-afba17a5ca4d"
/>
**Info messages (blue):**
- Borders a traitor ally: "You can betray traitors without becoming a
traitor yourself" (Because its not obvious for new players)
- Borders an allied AFK player: "You can attack disconnected players
even if you are allied with them" (Because its not obvious for new
players)
- Borders an AFK teammate: "You can attack disconnected teammates"
(Because its not obvious for new players)
Info messages only appear when the player has not attacked the relevant
neighbor for at least 15 seconds, so they do not show up without reason.
<img width="524" height="141" alt="image"
src="https://github.com/user-attachments/assets/88d74661-d47e-45a7-9f91-d4f5361114b7"
/>
New "Help Messages" toggle in settings (default: on)
<img width="409" height="105" alt="image"
src="https://github.com/user-attachments/assets/24bc8bed-777b-4f72-9451-02116ac39db0"
/>
Implementation details:
- Border detection uses async borderTiles() refreshed every 1s, cached
in a Set of nearby player smallIDs
- Outgoing attacks are tracked per-target to compute the 15-second idle
threshold
- New armyLimitWarningThreshold() on Config (returns 0.8)
- All user-facing strings go through translateText() with en.json
entries
AI Model used: MiMo 2.5 Pro
## 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
Suppress the "Alliance request sent" event when the action actually
accepts an existing incoming request rather than sending a new one.
This happens both when clicking "Accept" on a request card and when
sending a request to a player who already requested one from us — in
both cases EventsDisplay now checks whether the recipient is already
requesting an alliance with us and skips the confirmation.
Also clear stale incoming alliance request cards in ActionableEvents
once the request is resolved: handle AllianceRequestReply to remove the
card when accepted/rejected, and drop ALLIANCE_REQUEST cards in tick()
whose requestor is no longer requesting an alliance with us.
Resolves#4209
## Description:
While filtering events in the src/client/hud/layers/ActionableEvents.ts
tick function, we remove from the event list any event where the
requestor is not requesting an alliance anymore. This excludes alliance
requests where the requestor attacked (or bombed) AND alliance requests
where the recipient accepted through the radial menu.
## 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:
Katokoda
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.
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.
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.
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.
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
> **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
**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>
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
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.
## 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.
## 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
Resolves#3910
## Description:
- Split the events HUD into two components: a new
**`<actionable-events>`** that owns alliance prompts (request / renew)
and a slimmed-down **`<events-display>`** for everything else.
- Reworked `<events-display>` into two visual tiers: dim/scrolling tier
2 on top (trade results, unit losses, donations, alliance status),
prominent tier 1 anchored at the bottom (inbound nukes, naval invasion,
attack requests, alliance broken, conquered player, chat). Tier 2 caps
at the 4 newest entries; events expire after 8s.
- Added a transient **+gold pip** above the gold pill in
`<control-panel>`, animated with a small fade-in. Fires for trade ships,
trains, donations, and conquest. Trade-ship and train arrivals are
removed from the events scroll since they're surfaced here instead.
- New `MessageType.NUKE_DETONATED` and a server-side emission in
`NukeExecution.detonate` — once an inbound nuke lands or gets
intercepted, the inbound warning vanishes and a "detonated" entry takes
its place.
- `displayMessage` gained optional `unitID` and `focusPlayerID` params
so events can link to a unit or a player. Unit captures and destructions
now navigate to the unit's last tile when clicked; donations navigate to
the other player.
- ActionableEvents card width matches `<events-display>`; cards persist
until the user clicks Accept/Reject/Renew/Ignore or the server-side
request timeout expires.
- Removed the in-events category filter UI and the gold-amount banner —
`<events-display>` is now a lightweight log that hides entirely when
empty.
<img width="570" height="444" alt="Screenshot 2026-05-21 at 1 42 30 PM"
src="https://github.com/user-attachments/assets/f103efb3-0e11-4b72-a11b-91ff6896177c"
/>
<img width="430" height="296" alt="Screenshot 2026-05-21 at 1 41 34 PM"
src="https://github.com/user-attachments/assets/ae58475a-b252-4aa6-9ce5-99dea7575ce3"
/>
## 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 contents (Lit web components for in-game chat, build menu, leaderboard,
attack displays, etc.) are HUD, not graphics — the actual graphics is in
client/render/.