diff --git a/resources/lang/en.json b/resources/lang/en.json
index e7ae5f614..071730a05 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -972,6 +972,8 @@
"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)",
+ "coordinate_grid_opacity_label": "Coordinate grid opacity",
+ "coordinate_grid_opacity_desc": "How opaque the coordinate grid is (lower lets more things show through)",
"rail_distance_label": "Train track draw distance",
"rail_distance_desc": "How far zoomed out train tracks remain visible",
"rail_thickness_label": "Train track thickness",
diff --git a/src/client/hud/layers/GraphicsSettingsModal.ts b/src/client/hud/layers/GraphicsSettingsModal.ts
index b41842be6..d880e9515 100644
--- a/src/client/hud/layers/GraphicsSettingsModal.ts
+++ b/src/client/hud/layers/GraphicsSettingsModal.ts
@@ -52,6 +52,10 @@ const TERRITORY_ALPHA_MIN = 0;
const TERRITORY_ALPHA_MAX = 1;
const TERRITORY_ALPHA_STEP = 0.01;
+const COORDINATE_GRID_OPACITY_MIN = 0;
+const COORDINATE_GRID_OPACITY_MAX = 1;
+const COORDINATE_GRID_OPACITY_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;
@@ -252,6 +256,13 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
);
}
+ private currentCoordinateGridOpacity(): number {
+ return (
+ this.userSettings.graphicsOverrides().mapOverlay?.coordinateGridOpacity ??
+ renderDefaults.mapOverlay.coordinateGridOpacity
+ );
+ }
+
private currentRailMinZoom(): number {
return (
this.userSettings.graphicsOverrides().railroad?.railMinZoom ??
@@ -291,6 +302,11 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
this.patchMapOverlay({ territoryAlpha: value });
}
+ private onCoordinateGridOpacityChange(event: Event) {
+ const value = parseFloat((event.target as HTMLInputElement).value);
+ this.patchMapOverlay({ coordinateGridOpacity: value });
+ }
+
private onRailDrawDistanceChange(event: Event) {
const drawDistance = parseFloat((event.target as HTMLInputElement).value);
// Invert: higher draw distance => tracks visible when more zoomed out.
@@ -412,6 +428,7 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
const highlightThicken = this.currentHighlightThicken();
const territorySat = this.currentTerritorySat();
const territoryAlpha = this.currentTerritoryAlpha();
+ const coordinateGridOpacity = this.currentCoordinateGridOpacity();
const railDrawDistance = RAIL_ZOOM_MAX - this.currentRailMinZoom();
const railThickness = this.currentRailThickness();
const colorblind = this.currentColorblind();
@@ -751,6 +768,35 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
+
+
+
+ ${translateText(
+ "graphics_setting.coordinate_grid_opacity_label",
+ )}
+
+
+ ${translateText(
+ "graphics_setting.coordinate_grid_opacity_desc",
+ )}
+
+
+
+
+ ${coordinateGridOpacity.toFixed(2)}
+
+
+
diff --git a/src/client/render/gl/GraphicsOverrides.ts b/src/client/render/gl/GraphicsOverrides.ts
index 1a34dd585..916ed458a 100644
--- a/src/client/render/gl/GraphicsOverrides.ts
+++ b/src/client/render/gl/GraphicsOverrides.ts
@@ -24,6 +24,7 @@ export const GraphicsOverridesSchema = z
highlightThicken: z.number(),
territorySaturation: z.number(),
territoryAlpha: z.number(),
+ coordinateGridOpacity: z.number(),
})
.partial(),
railroad: z
diff --git a/src/client/render/gl/RenderOverrides.ts b/src/client/render/gl/RenderOverrides.ts
index 6c47f0c85..15c582d71 100644
--- a/src/client/render/gl/RenderOverrides.ts
+++ b/src/client/render/gl/RenderOverrides.ts
@@ -56,6 +56,10 @@ export function applyGraphicsOverrides(
if (overrides.mapOverlay?.territoryAlpha !== undefined) {
settings.mapOverlay.territoryAlpha = overrides.mapOverlay.territoryAlpha;
}
+ if (overrides.mapOverlay?.coordinateGridOpacity !== undefined) {
+ settings.mapOverlay.coordinateGridOpacity =
+ overrides.mapOverlay.coordinateGridOpacity;
+ }
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 511eafe5d..936790a6a 100644
--- a/src/client/render/gl/RenderSettings.ts
+++ b/src/client/render/gl/RenderSettings.ts
@@ -113,6 +113,7 @@ export interface RenderSettings {
territorySaturation: number;
/** Absolute opacity of the territory fill. 1 = fully opaque (terrain hidden), ~0.588 = default. */
territoryAlpha: number;
+ coordinateGridOpacity: number;
staleNukeBase: number;
staleNukeVariation: number;
staleNukeAlpha: number;
diff --git a/src/client/render/gl/Renderer.ts b/src/client/render/gl/Renderer.ts
index 3835f29df..b10fdaa4d 100644
--- a/src/client/render/gl/Renderer.ts
+++ b/src/client/render/gl/Renderer.ts
@@ -91,6 +91,8 @@ const SAM_RADIUS_HIGHLIGHT_TYPES = new Set([
"Hydrogen Bomb",
]);
+const GRID_VIEW_KEY = "renderer:grid_view_enabled";
+
export class GPURenderer {
private gl: WebGL2RenderingContext;
private camera: Camera;
@@ -526,6 +528,11 @@ export class GPURenderer {
mapH,
this.settings,
);
+ try {
+ this.gridView = window.localStorage.getItem(GRID_VIEW_KEY) === "true";
+ } catch {
+ this.setGridView(false);
+ }
for (const p of header.players) {
if (p.team !== null) this.playerTeams.set(p.smallID, p.team);
@@ -1078,6 +1085,11 @@ export class GPURenderer {
setGridView(active: boolean): void {
this.gridView = active;
+ try {
+ window.localStorage.setItem(GRID_VIEW_KEY, active ? "true" : "false");
+ } catch {
+ // Ignore if we are unable to use localstorage.
+ }
}
getSettings(): RenderSettings {
diff --git a/src/client/render/gl/passes/CoordinateGridPass.ts b/src/client/render/gl/passes/CoordinateGridPass.ts
index 5993a02d6..26c6116a6 100644
--- a/src/client/render/gl/passes/CoordinateGridPass.ts
+++ b/src/client/render/gl/passes/CoordinateGridPass.ts
@@ -31,6 +31,7 @@ export class CoordinateGridPass {
private uCellSize: WebGLUniformLocation;
private uZoom: WebGLUniformLocation;
private uFontSize: WebGLUniformLocation;
+ private uOpacity: WebGLUniformLocation;
private mapW: number;
private mapH: number;
@@ -57,6 +58,7 @@ export class CoordinateGridPass {
this.uCellSize = gl.getUniformLocation(this.program, "uCellSize")!;
this.uZoom = gl.getUniformLocation(this.program, "uZoom")!;
this.uFontSize = gl.getUniformLocation(this.program, "uFontSize")!;
+ this.uOpacity = gl.getUniformLocation(this.program, "uOpacity")!;
gl.useProgram(this.program);
gl.uniform1i(gl.getUniformLocation(this.program, "uGlyphTex"), 0);
@@ -73,6 +75,7 @@ export class CoordinateGridPass {
gl.uniform1f(this.uCellSize, this.cellSize);
gl.uniform1f(this.uZoom, zoom);
gl.uniform1f(this.uFontSize, this.settings.altView.gridFontSize);
+ gl.uniform1f(this.uOpacity, this.settings.mapOverlay.coordinateGridOpacity);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.glyphTex);
@@ -95,14 +98,16 @@ export class CoordinateGridPass {
canvas.height = GLYPH_H;
const ctx = canvas.getContext("2d")!;
- ctx.fillStyle = "black";
- ctx.fillRect(0, 0, canvas.width, canvas.height);
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.strokeStyle = "black";
+ ctx.lineWidth = 4;
ctx.fillStyle = "white";
ctx.font = `bold ${GLYPH_H - 8}px monospace`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for (let i = 0; i < CHARS.length; i++) {
+ ctx.strokeText(CHARS[i], i * GLYPH_W + GLYPH_W / 2, GLYPH_H / 2);
ctx.fillText(CHARS[i], i * GLYPH_W + GLYPH_W / 2, GLYPH_H / 2);
}
diff --git a/src/client/render/gl/render-settings.json b/src/client/render/gl/render-settings.json
index f52a6d7e7..d3664ec64 100644
--- a/src/client/render/gl/render-settings.json
+++ b/src/client/render/gl/render-settings.json
@@ -69,6 +69,7 @@
"territoryDefenseDarken": 0.85,
"territorySaturation": 1,
"territoryAlpha": 0.588,
+ "coordinateGridOpacity": 0.5,
"staleNukeBase": 0,
"staleNukeVariation": 0.05,
"staleNukeAlpha": 1,
diff --git a/src/client/render/gl/shaders/grid/grid.frag.glsl b/src/client/render/gl/shaders/grid/grid.frag.glsl
index 7174d0efa..193529956 100644
--- a/src/client/render/gl/shaders/grid/grid.frag.glsl
+++ b/src/client/render/gl/shaders/grid/grid.frag.glsl
@@ -6,6 +6,7 @@ uniform float uCellSize;
uniform float uZoom;
uniform float uFontSize;
uniform sampler2D uGlyphTex;
+uniform float uOpacity;
in vec2 vWorldPos;
out vec4 fragColor;
@@ -29,7 +30,7 @@ void main() {
// --- Grid lines (at cell boundaries) ---
if (localX < lineW || localY < lineW) {
- fragColor = vec4(1.0, 1.0, 1.0, 0.35);
+ fragColor = vec4(1.0, 1.0, 1.0, uOpacity);
return;
}
@@ -41,7 +42,6 @@ void main() {
float gw = fontSize * 0.6 * px; // glyph width in world units
float gh = fontSize * px; // glyph height
float pad = 8.0 * px; // padding from cell corner
- float bgPad = 2.0 * px; // background extends beyond text
float lx = localX - pad;
float ly = localY - pad;
@@ -73,32 +73,24 @@ void main() {
float totalW = float(nc) * gw;
- // Check label background area (text + padding)
- if (lx < -bgPad || ly < -bgPad || lx >= totalW + bgPad || ly >= gh + bgPad)
- discard;
-
// Check if on actual glyph
- if (lx >= 0.0 && ly >= 0.0 && lx < totalW && ly < gh) {
- int ci = int(floor(lx / gw));
- if (ci < nc) {
- int g;
- if (ci == 0) g = c0;
- else if (ci == 1) g = c1;
- else if (ci == 2) g = c2;
- else g = c3;
-
- float cu = fract(lx / gw);
- float cv = ly / gh;
- float au = (float(g) + cu) / GLYPH_COUNT;
- float mask = texture(uGlyphTex, vec2(au, cv)).r;
-
- if (mask > 0.3) {
- fragColor = vec4(1.0, 1.0, 1.0, 0.9);
- return;
- }
- }
+ if (lx < 0.0 || ly < 0.0 || lx >= totalW || ly >= gh) {
+ discard;
}
- // Background behind label
- fragColor = vec4(0.08, 0.08, 0.08, 0.7);
+ int ci = int(floor(lx / gw));
+ if (ci < nc) {
+ int g;
+ if (ci == 0) g = c0;
+ else if (ci == 1) g = c1;
+ else if (ci == 2) g = c2;
+ else g = c3;
+
+ float cu = fract(lx / gw);
+ float cv = ly / gh;
+ float au = (float(g) + cu) / GLYPH_COUNT;
+ vec4 gColor = texture(uGlyphTex, vec2(au, cv));
+ fragColor = gColor.a * vec4(gColor.rgb, uOpacity);
+ return;
+ }
}