From 2d28d5463ba3a6b428e970c7ec978d0846f86fe3 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 9 Jun 2026 19:15:54 -0700 Subject: [PATCH] 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. --- resources/lang/en.json | 4 + .../hud/layers/GraphicsSettingsModal.ts | 84 +++++++++++++++++++ src/client/render/gl/GraphicsOverrides.ts | 2 + src/client/render/gl/RenderOverrides.ts | 7 ++ src/client/render/gl/RenderSettings.ts | 4 + src/client/render/gl/debug/Layout.ts | 18 ++++ src/client/render/gl/passes/TerritoryPass.ts | 9 ++ src/client/render/gl/render-settings.json | 2 + .../shaders/map-overlay/territory.frag.glsl | 10 +++ tests/GraphicsOverrides.test.ts | 44 ++++++++++ 10 files changed, 184 insertions(+) diff --git a/resources/lang/en.json b/resources/lang/en.json index 7d383594c..613c2e7c7 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -945,6 +945,10 @@ "highlight_brighten_desc": "How strongly the border brightens on hover (0 to disable)", "highlight_thicken_label": "Border highlight thickness", "highlight_thicken_desc": "How much the border thickens on hover", + "territory_sat_label": "Territory saturation", + "territory_sat_desc": "How vivid the territory fill colors are (lower mutes them)", + "territory_alpha_label": "Territory opacity", + "territory_alpha_desc": "How opaque the territory fill is (lower lets terrain show through)", "rail_distance_label": "Train track draw distance", "rail_distance_desc": "How far zoomed out train tracks remain visible", "section_effects": "Effects", diff --git a/src/client/hud/layers/GraphicsSettingsModal.ts b/src/client/hud/layers/GraphicsSettingsModal.ts index 8ea255318..4cad92c4e 100644 --- a/src/client/hud/layers/GraphicsSettingsModal.ts +++ b/src/client/hud/layers/GraphicsSettingsModal.ts @@ -32,6 +32,14 @@ const HIGHLIGHT_THICKEN_MIN = 0; const HIGHLIGHT_THICKEN_MAX = 5; const HIGHLIGHT_THICKEN_STEP = 1; +const TERRITORY_SAT_MIN = 0; +const TERRITORY_SAT_MAX = 1; +const TERRITORY_SAT_STEP = 0.01; + +const TERRITORY_ALPHA_MIN = 0; +const TERRITORY_ALPHA_MAX = 1; +const TERRITORY_ALPHA_STEP = 0.01; + // Train track "draw distance" is presented inverted: a higher slider value means // tracks stay visible when more zoomed out, i.e. a lower railMinZoom. const RAIL_ZOOM_MIN = 0; @@ -193,6 +201,20 @@ export class GraphicsSettingsModal extends LitElement implements Controller { ); } + private currentTerritorySat(): number { + return ( + this.userSettings.graphicsOverrides().mapOverlay?.territorySaturation ?? + renderDefaults.mapOverlay.territorySaturation + ); + } + + private currentTerritoryAlpha(): number { + return ( + this.userSettings.graphicsOverrides().mapOverlay?.territoryAlpha ?? + renderDefaults.mapOverlay.territoryAlpha + ); + } + private currentRailMinZoom(): number { return ( this.userSettings.graphicsOverrides().railroad?.railMinZoom ?? @@ -215,6 +237,16 @@ export class GraphicsSettingsModal extends LitElement implements Controller { this.patchMapOverlay({ highlightThicken: value }); } + private onTerritorySatChange(event: Event) { + const value = parseFloat((event.target as HTMLInputElement).value); + this.patchMapOverlay({ territorySaturation: value }); + } + + private onTerritoryAlphaChange(event: Event) { + const value = parseFloat((event.target as HTMLInputElement).value); + this.patchMapOverlay({ territoryAlpha: value }); + } + private onRailDrawDistanceChange(event: Event) { const drawDistance = parseFloat((event.target as HTMLInputElement).value); // Invert: higher draw distance => tracks visible when more zoomed out. @@ -287,6 +319,8 @@ export class GraphicsSettingsModal extends LitElement implements Controller { const highlightFill = this.currentHighlightFill(); const highlightBrighten = this.currentHighlightBrighten(); const highlightThicken = this.currentHighlightThicken(); + const territorySat = this.currentTerritorySat(); + const territoryAlpha = this.currentTerritoryAlpha(); const railDrawDistance = RAIL_ZOOM_MAX - this.currentRailMinZoom(); return html` @@ -499,6 +533,56 @@ export class GraphicsSettingsModal extends LitElement implements Controller { +
+
+
+ ${translateText("graphics_setting.territory_sat_label")} +
+
+ ${translateText("graphics_setting.territory_sat_desc")} +
+ +
+
+ ${territorySat.toFixed(2)} +
+
+ +
+
+
+ ${translateText("graphics_setting.territory_alpha_label")} +
+
+ ${translateText("graphics_setting.territory_alpha_desc")} +
+ +
+
+ ${territoryAlpha.toFixed(2)} +
+
+
diff --git a/src/client/render/gl/GraphicsOverrides.ts b/src/client/render/gl/GraphicsOverrides.ts index 6522ff5fe..9f6ff26b5 100644 --- a/src/client/render/gl/GraphicsOverrides.ts +++ b/src/client/render/gl/GraphicsOverrides.ts @@ -19,6 +19,8 @@ export const GraphicsOverridesSchema = z highlightFillBrighten: z.number(), highlightBrighten: z.number(), highlightThicken: z.number(), + territorySaturation: z.number(), + territoryAlpha: z.number(), }) .partial(), railroad: z diff --git a/src/client/render/gl/RenderOverrides.ts b/src/client/render/gl/RenderOverrides.ts index 0b146d2bc..e44d5f3f4 100644 --- a/src/client/render/gl/RenderOverrides.ts +++ b/src/client/render/gl/RenderOverrides.ts @@ -35,6 +35,13 @@ export function applyGraphicsOverrides( settings.mapOverlay.highlightThicken = overrides.mapOverlay.highlightThicken; } + if (overrides.mapOverlay?.territorySaturation !== undefined) { + settings.mapOverlay.territorySaturation = + overrides.mapOverlay.territorySaturation; + } + if (overrides.mapOverlay?.territoryAlpha !== undefined) { + settings.mapOverlay.territoryAlpha = overrides.mapOverlay.territoryAlpha; + } if (overrides.railroad?.railMinZoom !== undefined) { settings.railroad.railMinZoom = overrides.railroad.railMinZoom; } diff --git a/src/client/render/gl/RenderSettings.ts b/src/client/render/gl/RenderSettings.ts index f3387defe..3adfb9096 100644 --- a/src/client/render/gl/RenderSettings.ts +++ b/src/client/render/gl/RenderSettings.ts @@ -69,6 +69,10 @@ export interface RenderSettings { trailAlpha: number; defenseCheckerDarken: number; territoryDefenseDarken: number; + /** Saturation of the territory fill. 1 = full color, 0 = grayscale. */ + territorySaturation: number; + /** Absolute opacity of the territory fill. 1 = fully opaque (terrain hidden), ~0.588 = default. */ + territoryAlpha: number; staleNukeBase: number; staleNukeVariation: number; staleNukeAlpha: number; diff --git a/src/client/render/gl/debug/Layout.ts b/src/client/render/gl/debug/Layout.ts index 66ebef676..ef4c7d813 100644 --- a/src/client/render/gl/debug/Layout.ts +++ b/src/client/render/gl/debug/Layout.ts @@ -123,6 +123,24 @@ export function buildTree(s: RenderSettings, d: RenderSettings): DebugNode[] { slider(s.mapOverlay, "trailAlpha", d.mapOverlay, 0, 1, 0.01), slider(s.mapOverlay, "defenseCheckerDarken", d.mapOverlay, 0, 1, 0.01), slider(s.mapOverlay, "territoryDefenseDarken", d.mapOverlay, 0, 1, 0.01), + slider( + s.mapOverlay, + "territorySaturation", + d.mapOverlay, + 0, + 1, + 0.01, + "Territory Saturation", + ), + slider( + s.mapOverlay, + "territoryAlpha", + d.mapOverlay, + 0, + 1, + 0.01, + "Territory Alpha", + ), slider(s.mapOverlay, "staleNukeBase", d.mapOverlay, 0, 0.3, 0.005), slider(s.mapOverlay, "staleNukeVariation", d.mapOverlay, 0, 0.3, 0.005), slider(s.mapOverlay, "staleNukeAlpha", d.mapOverlay, 0, 1, 0.01), diff --git a/src/client/render/gl/passes/TerritoryPass.ts b/src/client/render/gl/passes/TerritoryPass.ts index a94a897e6..3139eaf74 100644 --- a/src/client/render/gl/passes/TerritoryPass.ts +++ b/src/client/render/gl/passes/TerritoryPass.ts @@ -41,6 +41,8 @@ export class TerritoryPass { private uShowPatterns: WebGLUniformLocation; private uIsTeamMode: WebGLUniformLocation; private uDefenseDarken: WebGLUniformLocation; + private uSaturation: WebGLUniformLocation; + private uTerritoryAlpha: WebGLUniformLocation; private highlightOwner = 0; private isTeamMode = false; @@ -165,6 +167,11 @@ export class TerritoryPass { this.program, "uDefenseDarken", )!; + this.uSaturation = gl.getUniformLocation(this.program, "uSaturation")!; + this.uTerritoryAlpha = gl.getUniformLocation( + this.program, + "uTerritoryAlpha", + )!; gl.useProgram(this.program); gl.uniform1i(gl.getUniformLocation(this.program, "uTileTex"), 0); @@ -458,6 +465,8 @@ export class TerritoryPass { ); gl.uniform1i(this.uIsTeamMode, this.isTeamMode ? 1 : 0); gl.uniform1f(this.uDefenseDarken, mo.territoryDefenseDarken); + gl.uniform1f(this.uSaturation, mo.territorySaturation); + gl.uniform1f(this.uTerritoryAlpha, mo.territoryAlpha); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.tileTex); diff --git a/src/client/render/gl/render-settings.json b/src/client/render/gl/render-settings.json index ecf9852af..d9e85f4d6 100644 --- a/src/client/render/gl/render-settings.json +++ b/src/client/render/gl/render-settings.json @@ -67,6 +67,8 @@ "trailAlpha": 0.588, "defenseCheckerDarken": 0.7, "territoryDefenseDarken": 0.85, + "territorySaturation": 1, + "territoryAlpha": 0.588, "staleNukeBase": 0, "staleNukeVariation": 0.05, "staleNukeAlpha": 1, diff --git a/src/client/render/gl/shaders/map-overlay/territory.frag.glsl b/src/client/render/gl/shaders/map-overlay/territory.frag.glsl index 4a6b5e128..d3f472d53 100644 --- a/src/client/render/gl/shaders/map-overlay/territory.frag.glsl +++ b/src/client/render/gl/shaders/map-overlay/territory.frag.glsl @@ -25,6 +25,8 @@ uniform float uHighlightBrighten; // hover contrast boost strength; 0 = disable uniform sampler2D uDefenseCoverageTex; // R8 — 1.0 = tile defended by same-owner post uniform float uDefenseDarken; // multiplier applied to fill on defended tiles uniform sampler2D uBorderTex; // RGBA8 — border flags; R > 0.25 = border tile +uniform float uSaturation; // 1 = full color, 0 = grayscale +uniform float uTerritoryAlpha; // absolute fill opacity; 1 = fully opaque in vec2 vWorldPos; out vec4 fragColor; @@ -121,5 +123,13 @@ void main() { color.rgb *= uDefenseDarken; } + // Adjust how saturated the fill is by blending toward its luminance. + if (uSaturation != 1.0) { + float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114)); + color.rgb = mix(vec3(luma), color.rgb, uSaturation); + } + + color.a = uTerritoryAlpha; + fragColor = color; } diff --git a/tests/GraphicsOverrides.test.ts b/tests/GraphicsOverrides.test.ts index aedebc77b..570033f5c 100644 --- a/tests/GraphicsOverrides.test.ts +++ b/tests/GraphicsOverrides.test.ts @@ -42,6 +42,18 @@ describe("GraphicsOverridesSchema", () => { } }); + test("accepts partial mapOverlay overrides", () => { + const cases = [ + { mapOverlay: {} }, + { mapOverlay: { territorySaturation: 0.5 } }, + { mapOverlay: { territoryAlpha: 0.8 } }, + { mapOverlay: { territorySaturation: 0, territoryAlpha: 1 } }, + ]; + for (const c of cases) { + expect(GraphicsOverridesSchema.safeParse(c).success).toBe(true); + } + }); + test("rejects wrong field types", () => { expect( GraphicsOverridesSchema.safeParse({ name: { nameScaleFactor: "big" } }) @@ -55,6 +67,11 @@ describe("GraphicsOverridesSchema", () => { structure: { classicIcons: "yes" }, }).success, ).toBe(false); + expect( + GraphicsOverridesSchema.safeParse({ + mapOverlay: { territorySaturation: "full" }, + }).success, + ).toBe(false); }); }); @@ -170,6 +187,33 @@ describe("applyGraphicsOverrides", () => { expect(absent.iconAlpha).toBe(1); }); + test("applies territorySaturation override (including 0)", () => { + expect( + gen({ mapOverlay: { territorySaturation: 0.4 } }).mapOverlay + .territorySaturation, + ).toBe(0.4); + expect( + gen({ mapOverlay: { territorySaturation: 0 } }).mapOverlay + .territorySaturation, + ).toBe(0); + }); + + test("applies territoryAlpha override (including 0)", () => { + expect( + gen({ mapOverlay: { territoryAlpha: 0.3 } }).mapOverlay.territoryAlpha, + ).toBe(0.3); + expect( + gen({ mapOverlay: { territoryAlpha: 0 } }).mapOverlay.territoryAlpha, + ).toBe(0); + }); + + test("mapOverlay override leaves other mapOverlay fields at defaults", () => { + const defaults = createRenderSettings().mapOverlay; + const mo = gen({ mapOverlay: { territorySaturation: 0.2 } }).mapOverlay; + expect(mo.territoryAlpha).toBe(defaults.territoryAlpha); + expect(mo.territoryDefenseDarken).toBe(defaults.territoryDefenseDarken); + }); + test("classicIcons + name overrides compose independently", () => { const s = gen({ name: { darkNames: true, nameScaleFactor: 0.9 },