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
This commit is contained in:
Evan
2026-06-02 11:48:52 -07:00
committed by GitHub
parent 2386b4b38a
commit f1045a2022
12 changed files with 125 additions and 125 deletions
+48
View File
@@ -0,0 +1,48 @@
import type { GraphicsOverrides } from "./GraphicsOverrides";
import type { RenderSettings } from "./RenderSettings";
const DARK_AMBIENT = 0.35;
export function applyGraphicsOverrides(
settings: RenderSettings,
overrides: GraphicsOverrides,
): void {
if (overrides.name?.nameScaleFactor !== undefined) {
settings.name.nameScaleFactor = overrides.name.nameScaleFactor;
}
if (overrides.name?.cullThreshold !== undefined) {
settings.name.cullThreshold = overrides.name.cullThreshold;
}
if (overrides.structure?.classicIcons === true) {
// Classic look: lighter player-colored shape behind a dark icon glyph,
// with a touch of translucency.
settings.structure.borderDarken = 0.7;
settings.structure.fillDarken = 1.0;
settings.structure.iconR = 0;
settings.structure.iconG = 0;
settings.structure.iconB = 0;
settings.structure.iconAlpha = 0.75;
}
if (overrides.name?.darkNames !== undefined) {
const dark = overrides.name.darkNames;
// Dark: black fill + player-colored outline. Force outline RGB to black
// so the shader's defaultFill ramp (mix(uOutlineColor, black, fillT))
// collapses to pure black regardless of ambient.
// Colored: player-colored fill + white outline (defaults from JSON).
settings.name.fillUsePlayerColor = !dark;
settings.name.outlineUsePlayerColor = dark;
const channel = dark ? 0 : 1;
settings.name.outlineR = channel;
settings.name.outlineG = channel;
settings.name.outlineB = channel;
}
}
export function applyDarkModeOverride(
settings: RenderSettings,
isDark: boolean,
): void {
if (!isDark) return;
settings.lighting.ambient = DARK_AMBIENT;
settings.lighting.enabled = true;
}
+3 -45
View File
@@ -1,4 +1,3 @@
import { GraphicsOverrides } from "./GraphicsOverrides";
import defaults from "./render-settings.json";
export interface RenderSettings {
@@ -47,10 +46,9 @@ export interface RenderSettings {
particleStrength: number;
particleFreshScale: number;
};
dayNight: {
mode: "light" | "dark";
nightAmbient: number;
dayAmbient: number;
lighting: {
ambient: number;
enabled: boolean;
falloffPower: number;
falloutLightR: number;
falloutLightG: number;
@@ -296,46 +294,6 @@ export function createRenderSettings(): RenderSettings {
return JSON.parse(JSON.stringify(defaults)) as RenderSettings;
}
/**
* Generate a fresh RenderSettings by layering user overrides on top of the
* render-settings.json defaults. Pure — does not mutate any input.
*/
export function generateRenderSettings(
overrides: GraphicsOverrides,
): RenderSettings {
const settings = createRenderSettings();
if (overrides.name?.nameScaleFactor !== undefined) {
settings.name.nameScaleFactor = overrides.name.nameScaleFactor;
}
if (overrides.name?.cullThreshold !== undefined) {
settings.name.cullThreshold = overrides.name.cullThreshold;
}
if (overrides.structure?.classicIcons === true) {
// Classic look: lighter player-colored shape behind a dark icon glyph,
// with a touch of translucency.
settings.structure.borderDarken = 0.7;
settings.structure.fillDarken = 1.0;
settings.structure.iconR = 0;
settings.structure.iconG = 0;
settings.structure.iconB = 0;
settings.structure.iconAlpha = 0.75;
}
if (overrides.name?.darkNames !== undefined) {
const dark = overrides.name.darkNames;
// Dark: black fill + player-colored outline. Force outline RGB to black
// so the shader's defaultFill ramp (mix(uOutlineColor, black, fillT))
// collapses to pure black regardless of ambient.
// Colored: player-colored fill + white outline (defaults from JSON).
settings.name.fillUsePlayerColor = !dark;
settings.name.outlineUsePlayerColor = dark;
const channel = dark ? 0 : 1;
settings.name.outlineR = channel;
settings.name.outlineG = channel;
settings.name.outlineB = channel;
}
return settings;
}
/** Dump current settings to a downloadable JSON file. */
export function dumpSettings(settings: RenderSettings): void {
const json = JSON.stringify(settings, null, 2);
+4 -4
View File
@@ -1208,9 +1208,9 @@ export class GPURenderer {
const zoom = this.camera.zoom;
const cw = this.canvas.width;
const ch = this.canvas.height;
const nightActive = this.isNightActive();
const compositingActive = this.isLightCompositingActive();
if (nightActive) {
if (compositingActive) {
this.resizeSceneTargetIfNeeded(cw, ch);
const sceneTex = toTarget(this.gl, this.sceneTarget, () =>
this.drawBaseLayer(cam),
@@ -1226,8 +1226,8 @@ export class GPURenderer {
this.renderOverlays(cam, zoom);
}
private isNightActive(): boolean {
return this.settings.dayNight.mode === "dark";
private isLightCompositingActive(): boolean {
return this.settings.lighting.enabled;
}
private resizeSceneTargetIfNeeded(cw: number, ch: number): void {
+13 -15
View File
@@ -2,7 +2,6 @@ import type { RenderSettings } from "../RenderSettings";
import type { DebugNode } from "./Folder";
import { folder } from "./Folder";
import { color } from "./props/Color";
import { select } from "./props/Select";
import { slider } from "./props/Slider";
import { toggle } from "./props/Toggle";
@@ -90,30 +89,29 @@ export function buildTree(s: RenderSettings, d: RenderSettings): DebugNode[] {
slider(s.falloutBloom, "particleFreshScale", d.falloutBloom, 0, 1, 0.01),
]),
folder("Day / Night", [
select(s.dayNight, "mode", d.dayNight, ["light", "dark"], "Mode"),
slider(s.dayNight, "nightAmbient", d.dayNight, 0, 1, 0.01),
slider(s.dayNight, "dayAmbient", d.dayNight, 0, 1, 0.01),
slider(s.dayNight, "falloffPower", d.dayNight, 0.5, 5, 0.1),
slider(s.dayNight, "falloutLightIntensity", d.dayNight, 0, 20, 0.1),
slider(s.dayNight, "falloutLightThreshold", d.dayNight, 0, 0.5, 0.001),
slider(s.dayNight, "blurZoomDivisor", d.dayNight, 1, 20, 0.5),
slider(s.dayNight, "lightRadiusMultiplier", d.dayNight, 0.1, 5, 0.1),
folder("Lighting", [
toggle(s.lighting, "enabled", d.lighting),
slider(s.lighting, "ambient", d.lighting, 0, 1, 0.01),
slider(s.lighting, "falloffPower", d.lighting, 0.5, 5, 0.1),
slider(s.lighting, "falloutLightIntensity", d.lighting, 0, 20, 0.1),
slider(s.lighting, "falloutLightThreshold", d.lighting, 0, 0.5, 0.001),
slider(s.lighting, "blurZoomDivisor", d.lighting, 1, 20, 0.5),
slider(s.lighting, "lightRadiusMultiplier", d.lighting, 0.1, 5, 0.1),
color(
s.dayNight,
s.lighting,
"falloutLightR",
"falloutLightG",
"falloutLightB",
d.dayNight,
d.lighting,
"Fallout Light Color",
),
slider(s.dayNight, "emberLightIntensity", d.dayNight, 0, 20, 0.1),
slider(s.lighting, "emberLightIntensity", d.lighting, 0, 20, 0.1),
color(
s.dayNight,
s.lighting,
"emberLightR",
"emberLightG",
"emberLightB",
d.dayNight,
d.lighting,
"Ember Light Color",
),
]),
+4 -4
View File
@@ -13,10 +13,10 @@ export { GraphicsOverridesSchema } from "./GraphicsOverrides";
export type { GraphicsOverrides } from "./GraphicsOverrides";
export type { SpawnCenter } from "./passes/SpawnOverlayPass";
export {
createRenderSettings,
dumpSettings,
generateRenderSettings,
} from "./RenderSettings";
applyDarkModeOverride,
applyGraphicsOverrides,
} from "./RenderOverrides";
export { createRenderSettings, dumpSettings } from "./RenderSettings";
export type { RenderSettings } from "./RenderSettings";
export { deepAssign, deepDiff } from "./SettingsUtils";
export { buildTerrainRGBA, getPaletteSize } from "./utils/ColorUtils";
@@ -192,7 +192,7 @@ export class FalloutLightPass {
tick: number,
): void {
const gl = this.gl;
const dn = this.settings.dayNight;
const dn = this.settings.lighting;
const fb = this.settings.falloutBloom;
// Step 1: Extract fallout light in tile space
+1 -1
View File
@@ -166,7 +166,7 @@ export class LightmapPass {
const zoom = Math.abs(cameraMatrix[0]);
const mapSize = Math.max(this.mapW, this.mapH);
const blurScale = Math.min(
(zoom * mapSize) / this.settings.dayNight.blurZoomDivisor,
(zoom * mapSize) / this.settings.lighting.blurZoomDivisor,
1.0,
);
@@ -47,8 +47,7 @@ export class NightCompositePass {
// -------------------------------------------------------------------------
getAmbient(): number {
const dn = this.settings.dayNight;
return dn.mode === "dark" ? dn.nightAmbient : dn.dayAmbient;
return this.settings.lighting.ambient;
}
// -------------------------------------------------------------------------
@@ -207,7 +207,7 @@ export class PointLightPass {
if (this.lightCount === 0) return;
const gl = this.gl;
const dn = this.settings.dayNight;
const dn = this.settings.lighting;
gl.useProgram(this.lightProg);
gl.uniformMatrix3fv(this.uLightCam, false, cameraMatrix);
+3 -4
View File
@@ -44,10 +44,9 @@
"particleStrength": 1,
"particleFreshScale": 0.2
},
"dayNight": {
"mode": "light",
"nightAmbient": 0.15,
"dayAmbient": 1,
"lighting": {
"ambient": 1,
"enabled": false,
"falloffPower": 2,
"falloutLightR": 0.15,
"falloutLightG": 0.95,