mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 04:03:30 +00:00
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:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user