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
This commit is contained in:
Evan
2026-05-28 13:06:43 -07:00
committed by GitHub
parent 8142bc1070
commit 20bc311caf
11 changed files with 401 additions and 7 deletions
+24 -1
View File
@@ -31,6 +31,7 @@ import { GameView, PlayerView } from "../core/game/GameView";
import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader";
import {
DARK_MODE_KEY,
GRAPHICS_KEY,
USER_SETTINGS_CHANGED_EVENT,
UserSettings,
} from "../core/game/UserSettings";
@@ -67,7 +68,11 @@ import {
import { createCanvas } from "./Utils";
import { WebGLFrameBuilder } from "./WebGLFrameBuilder";
import { createRenderer, GameRenderer } from "./hud/GameRenderer";
import { createDebugGui, GameView as WebGLGameView } from "./render/gl";
import {
createDebugGui,
generateRenderSettings,
GameView as WebGLGameView,
} from "./render/gl";
import { ALL_UNIT_TYPES, UnitState } from "./render/types";
import { SoundManager } from "./sound/SoundManager";
@@ -479,6 +484,21 @@ async function createClientGame(
(e) => view.setShowPatterns((e as CustomEvent<string>).detail === "true"),
);
const graphicsListenerAbort = new AbortController();
const applyGraphicsOverrides = (): void => {
const generated = generateRenderSettings(
userSettings.graphicsOverrides(),
);
const live = view.getSettings();
Object.assign(live.name, generated.name);
};
applyGraphicsOverrides();
globalThis.addEventListener(
`${USER_SETTINGS_CHANGED_EVENT}:${GRAPHICS_KEY}`,
applyGraphicsOverrides,
{ signal: graphicsListenerAbort.signal },
);
let debugGui: ReturnType<typeof createDebugGui> | null = null;
eventBus.on(ToggleRenderDebugGuiEvent, () => {
if (debugGui === null) {
@@ -524,6 +544,7 @@ async function createClientGame(
soundManager,
userSettings,
webglBuilder,
graphicsListenerAbort,
);
} catch (err) {
soundManager.dispose();
@@ -557,6 +578,7 @@ export class ClientGameRunner {
private soundManager: SoundManager,
private userSettings: UserSettings,
private webglBuilder: WebGLFrameBuilder | null = null,
private graphicsListenerAbort: AbortController | null = null,
) {
this.lastMessageTime = Date.now();
}
@@ -813,6 +835,7 @@ export class ClientGameRunner {
public stop() {
this.soundManager.dispose();
this.graphicsListenerAbort?.abort();
if (!this.isActive) return;
this.isActive = false;