diff --git a/resources/lang/en.json b/resources/lang/en.json
index 5c0be6cf4..3e725c55f 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -958,6 +958,8 @@
"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",
+ "rail_thickness_label": "Train track thickness",
+ "rail_thickness_desc": "How wide train tracks are drawn",
"section_effects": "Effects",
"reset_label": "Reset to defaults",
"reset_desc": "Clear all graphics overrides"
diff --git a/src/client/WebGLFrameBuilder.ts b/src/client/WebGLFrameBuilder.ts
index 72aec8b3b..7ce81389a 100644
--- a/src/client/WebGLFrameBuilder.ts
+++ b/src/client/WebGLFrameBuilder.ts
@@ -109,10 +109,15 @@ export class WebGLFrameBuilder {
}
private syncLocalPlayer(gameView: GameView): void {
- const sid = gameView.myPlayer()?.smallID() ?? 0;
+ const me = gameView.myPlayer();
+ const sid = me?.smallID() ?? 0;
if (sid === this.localPlayerSmallID) return;
this.localPlayerSmallID = sid;
this.view.setLocalPlayerID(sid);
+ if (me) {
+ const rail = me.railColor().toRgb();
+ this.view.setLocalRailColor(rail.r / 255, rail.g / 255, rail.b / 255);
+ }
}
/**
diff --git a/src/client/hud/layers/GraphicsSettingsModal.ts b/src/client/hud/layers/GraphicsSettingsModal.ts
index 4cad92c4e..5ffd316c1 100644
--- a/src/client/hud/layers/GraphicsSettingsModal.ts
+++ b/src/client/hud/layers/GraphicsSettingsModal.ts
@@ -46,6 +46,10 @@ const RAIL_ZOOM_MIN = 0;
const RAIL_ZOOM_MAX = 10;
const RAIL_ZOOM_STEP = 0.1;
+const RAIL_THICKNESS_MIN = 0.5;
+const RAIL_THICKNESS_MAX = 3;
+const RAIL_THICKNESS_STEP = 0.1;
+
export class ShowGraphicsSettingsModalEvent {
constructor(
public readonly isVisible: boolean = true,
@@ -222,6 +226,13 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
);
}
+ private currentRailThickness(): number {
+ return (
+ this.userSettings.graphicsOverrides().railroad?.railThickness ??
+ renderDefaults.railroad.railThickness
+ );
+ }
+
private onHighlightFillChange(event: Event) {
const value = parseFloat((event.target as HTMLInputElement).value);
this.patchMapOverlay({ highlightFillBrighten: value });
@@ -253,6 +264,11 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
this.patchRailroad({ railMinZoom: RAIL_ZOOM_MAX - drawDistance });
}
+ private onRailThicknessChange(event: Event) {
+ const value = parseFloat((event.target as HTMLInputElement).value);
+ this.patchRailroad({ railThickness: value });
+ }
+
private currentClassicIcons(): boolean {
return (
this.userSettings.graphicsOverrides().structure?.classicIcons ?? false
@@ -322,6 +338,7 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
const territorySat = this.currentTerritorySat();
const territoryAlpha = this.currentTerritoryAlpha();
const railDrawDistance = RAIL_ZOOM_MAX - this.currentRailMinZoom();
+ const railThickness = this.currentRailThickness();
return html`
+
+
+
+ ${translateText("graphics_setting.rail_thickness_label")}
+
+
+ ${translateText("graphics_setting.rail_thickness_desc")}
+
+
+
+
+ ${railThickness.toFixed(1)}
+
+
+
diff --git a/src/client/render/gl/GameView.ts b/src/client/render/gl/GameView.ts
index 9b6938480..19073ab91 100644
--- a/src/client/render/gl/GameView.ts
+++ b/src/client/render/gl/GameView.ts
@@ -384,6 +384,10 @@ export class GameView {
setLocalPlayerID(id: number): void {
this.renderer?.setLocalPlayerID(id);
}
+ /** Rail color for the local player (0–1 RGB). */
+ setLocalRailColor(r: number, g: number, b: number): void {
+ this.renderer?.setLocalRailColor(r, g, b);
+ }
setAltView(active: boolean): void {
this.renderer?.setAltView(active);
}
diff --git a/src/client/render/gl/GraphicsOverrides.ts b/src/client/render/gl/GraphicsOverrides.ts
index 9f6ff26b5..518f9583e 100644
--- a/src/client/render/gl/GraphicsOverrides.ts
+++ b/src/client/render/gl/GraphicsOverrides.ts
@@ -26,6 +26,7 @@ export const GraphicsOverridesSchema = z
railroad: z
.object({
railMinZoom: z.number(),
+ railThickness: z.number(),
})
.partial(),
passEnabled: z
diff --git a/src/client/render/gl/RenderOverrides.ts b/src/client/render/gl/RenderOverrides.ts
index e44d5f3f4..505a97188 100644
--- a/src/client/render/gl/RenderOverrides.ts
+++ b/src/client/render/gl/RenderOverrides.ts
@@ -45,6 +45,9 @@ export function applyGraphicsOverrides(
if (overrides.railroad?.railMinZoom !== undefined) {
settings.railroad.railMinZoom = overrides.railroad.railMinZoom;
}
+ if (overrides.railroad?.railThickness !== undefined) {
+ settings.railroad.railThickness = overrides.railroad.railThickness;
+ }
if (overrides.passEnabled?.fx !== undefined) {
settings.passEnabled.fx = overrides.passEnabled.fx;
}
diff --git a/src/client/render/gl/RenderSettings.ts b/src/client/render/gl/RenderSettings.ts
index 09ac2964e..984d176fb 100644
--- a/src/client/render/gl/RenderSettings.ts
+++ b/src/client/render/gl/RenderSettings.ts
@@ -112,6 +112,8 @@ export interface RenderSettings {
railFadeRange: number;
railDetailZoom: number;
railAlpha: number;
+ /** Track width multiplier (1 = default width). */
+ railThickness: number;
};
structure: {
iconSize: number;
diff --git a/src/client/render/gl/Renderer.ts b/src/client/render/gl/Renderer.ts
index 8ea85c237..27ca7c1fd 100644
--- a/src/client/render/gl/Renderer.ts
+++ b/src/client/render/gl/Renderer.ts
@@ -1026,6 +1026,11 @@ export class GPURenderer {
this.samRadiusPass.setLocalPlayer(id);
this.affiliationPalette.setLocalPlayer(id);
this.unitPass.setLocalPlayer(id);
+ this.railroadPass.setLocalPlayer(id);
+ }
+
+ setLocalRailColor(r: number, g: number, b: number): void {
+ this.railroadPass.setLocalRailColor(r, g, b);
}
setSAMRadiusVisible(visible: boolean): void {
diff --git a/src/client/render/gl/debug/Layout.ts b/src/client/render/gl/debug/Layout.ts
index ef4c7d813..49df28965 100644
--- a/src/client/render/gl/debug/Layout.ts
+++ b/src/client/render/gl/debug/Layout.ts
@@ -201,6 +201,15 @@ export function buildTree(s: RenderSettings, d: RenderSettings): DebugNode[] {
"Detail Zoom",
),
slider(s.railroad, "railAlpha", d.railroad, 0, 1, 0.01, "Alpha"),
+ slider(
+ s.railroad,
+ "railThickness",
+ d.railroad,
+ 0.5,
+ 3,
+ 0.1,
+ "Thickness",
+ ),
]),
]),
diff --git a/src/client/render/gl/passes/RailroadPass.ts b/src/client/render/gl/passes/RailroadPass.ts
index 517c7f47c..b590684b3 100644
--- a/src/client/render/gl/passes/RailroadPass.ts
+++ b/src/client/render/gl/passes/RailroadPass.ts
@@ -99,7 +99,10 @@ export class RailroadPass {
private uRailDetailZoom: WebGLUniformLocation;
private uRailAlpha: WebGLUniformLocation;
private uRailFade: WebGLUniformLocation;
+ private uRailThickness: WebGLUniformLocation;
private uGhostOwnerID: WebGLUniformLocation;
+ private uLocalPlayerID: WebGLUniformLocation;
+ private uLocalRailColor: WebGLUniformLocation;
private mapW: number;
private mapH: number;
@@ -112,6 +115,9 @@ export class RailroadPass {
private ghostRailDirty = false;
private ghostOwnerID = 0;
+ private localPlayerID = 0;
+ private localRailColor: [number, number, number] = [0.75, 0.75, 0.75];
+
constructor(
private gl: WebGL2RenderingContext,
mapW: number,
@@ -147,7 +153,19 @@ export class RailroadPass {
)!;
this.uRailAlpha = gl.getUniformLocation(this.program, "uRailAlpha")!;
this.uRailFade = gl.getUniformLocation(this.program, "uRailFade")!;
+ this.uRailThickness = gl.getUniformLocation(
+ this.program,
+ "uRailThickness",
+ )!;
this.uGhostOwnerID = gl.getUniformLocation(this.program, "uGhostOwnerID")!;
+ this.uLocalPlayerID = gl.getUniformLocation(
+ this.program,
+ "uLocalPlayerID",
+ )!;
+ this.uLocalRailColor = gl.getUniformLocation(
+ this.program,
+ "uLocalRailColor",
+ )!;
// Texture unit bindings + ghost defaults
gl.useProgram(this.program);
@@ -199,6 +217,15 @@ export class RailroadPass {
this.railroadDirty = true;
}
+ setLocalPlayer(smallID: number): void {
+ this.localPlayerID = smallID;
+ }
+
+ /** Rail color for the local player (0–1 RGB). */
+ setLocalRailColor(r: number, g: number, b: number): void {
+ this.localRailColor = [r, g, b];
+ }
+
/**
* Sub-upload terrain bytes for tiles that changed (water-nuke conversions).
* Keeps the R8UI water-detection texture in sync with the simulation.
@@ -321,7 +348,15 @@ export class RailroadPass {
gl.uniform1f(this.uRailDetailZoom, rs.railDetailZoom);
gl.uniform1f(this.uRailAlpha, rs.railAlpha);
gl.uniform1f(this.uRailFade, fade);
+ gl.uniform1f(this.uRailThickness, rs.railThickness);
gl.uniform1f(this.uGhostOwnerID, this.ghostOwnerID);
+ gl.uniform1f(this.uLocalPlayerID, this.localPlayerID);
+ gl.uniform3f(
+ this.uLocalRailColor,
+ this.localRailColor[0],
+ this.localRailColor[1],
+ this.localRailColor[2],
+ );
// Bind textures: 0=railroad, 1=tile, 2=palette, 3=terrain, 4=ghostRail
gl.activeTexture(gl.TEXTURE0);
diff --git a/src/client/render/gl/render-settings.json b/src/client/render/gl/render-settings.json
index 392b44346..7b757fe34 100644
--- a/src/client/render/gl/render-settings.json
+++ b/src/client/render/gl/render-settings.json
@@ -106,7 +106,8 @@
"railMinZoom": 4,
"railFadeRange": 2,
"railDetailZoom": 6,
- "railAlpha": 1
+ "railAlpha": 1,
+ "railThickness": 1
},
"structure": {
"iconSize": 50,
diff --git a/src/client/render/gl/shaders/railroad/railroad.frag.glsl b/src/client/render/gl/shaders/railroad/railroad.frag.glsl
index e89a06cd1..5afc3877a 100644
--- a/src/client/render/gl/shaders/railroad/railroad.frag.glsl
+++ b/src/client/render/gl/shaders/railroad/railroad.frag.glsl
@@ -13,7 +13,10 @@ uniform float uZoom;
uniform float uRailDetailZoom;
uniform float uRailAlpha;
uniform float uRailFade; // Zoom-based fade multiplier (0..1)
+uniform float uRailThickness; // Track width multiplier (1 = default)
uniform float uGhostOwnerID; // Player smallID for ghost rail color
+uniform float uLocalPlayerID; // Local player smallID (0 = none)
+uniform vec3 uLocalRailColor; // Rail color for the local player's rails
in vec2 vWorldPos;
out vec4 fragColor;
@@ -54,35 +57,55 @@ bool isBridgePixel(uint rt, ivec2 lp) {
return false;
}
-// Compute rail pixel coverage for a given rail type at fractional tile position.
-// Returns 0.0 for miss, 1.0 for hit (detailed mode), or AA coverage (line mode).
-float railCoverage(uint rt, vec2 f) {
+// Detailed-mode coverage: 3x3 sub-grid with cross-ties, rail band width
+// scaled by uRailThickness (clamped so the two bands never overlap fully).
+float railDetailCoverage(uint rt, vec2 f) {
if (rt == 0u) return 0.0;
-
- if (uZoom >= uRailDetailZoom) {
- // Detailed mode: 3x3 sub-grid with cross-ties
- float T = 1.0 / 3.0;
- float T2 = 2.0 / 3.0;
- bool center = (f.x >= T && f.x < T2 && f.y >= T && f.y < T2);
- bool hit = false;
- if (rt == 1u) {
- hit = (f.x < T) || (f.x >= T2) || center;
- } else if (rt == 2u) {
- hit = (f.y < T) || (f.y >= T2) || center;
- } else if (rt == 3u) {
- hit = (f.y < T) || (f.x < T) || center;
- } else if (rt == 4u) {
- hit = (f.y < T) || (f.x >= T2) || center;
- } else if (rt == 5u) {
- hit = (f.y >= T2) || (f.x < T) || center;
- } else if (rt == 6u) {
- hit = (f.y >= T2) || (f.x >= T2) || center;
- }
- return hit ? 1.0 : 0.0;
- } else {
- // Simplified mode: fill entire tile (tiles are small at this zoom)
- return 1.0;
+ float T = 1.0 / 3.0;
+ float T2 = 2.0 / 3.0;
+ float w = min(T * uRailThickness, 0.5);
+ bool center = (f.x >= T && f.x < T2 && f.y >= T && f.y < T2);
+ bool hit = false;
+ if (rt == 1u) {
+ hit = (f.x < w) || (f.x >= 1.0 - w) || center;
+ } else if (rt == 2u) {
+ hit = (f.y < w) || (f.y >= 1.0 - w) || center;
+ } else if (rt == 3u) {
+ hit = (f.y < w) || (f.x < w) || center;
+ } else if (rt == 4u) {
+ hit = (f.y < w) || (f.x >= 1.0 - w) || center;
+ } else if (rt == 5u) {
+ hit = (f.y >= 1.0 - w) || (f.x < w) || center;
+ } else if (rt == 6u) {
+ hit = (f.y >= 1.0 - w) || (f.x >= 1.0 - w) || center;
}
+ return hit ? 1.0 : 0.0;
+}
+
+float segDist(vec2 p, vec2 a, vec2 b) {
+ vec2 ab = b - a;
+ float t = clamp(dot(p - a, ab) / dot(ab, ab), 0.0, 1.0);
+ return length(p - a - ab * t);
+}
+
+// Distance from tile-local point p to the rail centerline of type rt.
+// Straights span the tile; corners are two half-segments meeting at center.
+float railLineDist(uint rt, vec2 p) {
+ if (rt == 1u) return segDist(p, vec2(0.5, 0.0), vec2(0.5, 1.0));
+ if (rt == 2u) return segDist(p, vec2(0.0, 0.5), vec2(1.0, 0.5));
+ vec2 c = vec2(0.5);
+ vec2 e1 = vec2(0.5, (rt == 3u || rt == 4u) ? 0.0 : 1.0); // top or bottom edge
+ vec2 e2 = vec2((rt == 3u || rt == 5u) ? 0.0 : 1.0, 0.5); // left or right edge
+ return min(segDist(p, c, e1), segDist(p, c, e2));
+}
+
+// Line-mode coverage: screen-space anti-aliased line of width uRailThickness
+// (in tiles) around the rail centerline of tile-local point p.
+float railLineCoverage(uint rt, vec2 p) {
+ if (rt == 0u || rt > 6u) return 0.0;
+ float halfW = 0.5 * uRailThickness;
+ float aa = 0.5 / uZoom; // ~1 screen pixel in tile units
+ return 1.0 - smoothstep(halfW - aa, halfW + aa, railLineDist(rt, p));
}
void main() {
@@ -95,29 +118,34 @@ void main() {
uint ghostRailType = texelFetch(uGhostRailTex, tc, 0).r;
vec2 f = fract(vWorldPos);
- // Compute coverage for real and ghost rails
- float realCov = railCoverage(railType, f);
+ bool detailMode = uZoom >= uRailDetailZoom;
+
+ // Compute coverage for real and ghost rails. In line mode, real coverage is
+ // accumulated from the 3x3 neighborhood below so thick lines can spill into
+ // neighboring tiles.
+ float realCov = detailMode ? railDetailCoverage(railType, f) : 0.0;
// Ghost only renders where there is no real rail (values 1-6 = ghost path)
// Value 7 = highlight marker (existing rail turns green)
float ghostCov = (ghostRailType >= 1u && ghostRailType <= 6u && railType == 0u)
- ? railCoverage(ghostRailType, f)
+ ? (detailMode ? railDetailCoverage(ghostRailType, f) : railLineCoverage(ghostRailType, f))
: 0.0;
bool highlighted = (ghostRailType == 7u && railType != 0u);
- bool hitRail = (realCov * uRailAlpha > 0.001);
- bool hitGhost = (ghostCov * uRailAlpha > 0.001);
-
- // --- Bridge: check 3x3 neighborhood for water+rail tiles ---
+ // --- 3x3 neighborhood: bridges (both modes) + line coverage (line mode) ---
bool hitBridge = false;
ivec2 fp = ivec2(floor(vWorldPos * 2.0)); // fragment pos in game's 2x-pixel grid
- for (int dy = -1; dy <= 1 && !hitBridge; dy++) {
- for (int dx = -1; dx <= 1 && !hitBridge; dx++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ for (int dx = -1; dx <= 1; dx++) {
ivec2 ntc = tc + ivec2(dx, dy);
if (ntc.x < 0 || ntc.y < 0 || ntc.x >= int(uMapSize.x) || ntc.y >= int(uMapSize.y))
continue;
uint nRail = texelFetch(uRailroadTex, ntc, 0).r;
if (nRail == 0u) continue;
+ if (!detailMode) {
+ realCov = max(realCov, railLineCoverage(nRail, vWorldPos - vec2(ntc)));
+ }
+ if (hitBridge) continue;
uint nTerr = texelFetch(uTerrainTex, ntc, 0).r;
if ((nTerr & 0x80u) != 0u) continue; // land tile, no bridge
ivec2 lp = fp - ntc * 2;
@@ -125,6 +153,9 @@ void main() {
}
}
+ bool hitRail = (realCov * uRailAlpha > 0.001);
+ bool hitGhost = (ghostCov * uRailAlpha > 0.001);
+
if (!hitBridge && !hitRail && !hitGhost) discard;
// --- Color output ---
@@ -134,9 +165,13 @@ void main() {
float railAlpha = uRailAlpha * realCov;
uint tileRaw = texelFetch(uTileTex, tc, 0).r;
uint owner = tileRaw & uint(OWNER_MASK);
- vec3 railColor = owner != 0u
- ? texture(uPalette, vec2((float(owner) + 0.5) / float(PALETTE_SIZE), 0.75)).rgb
- : vec3(0.75);
+ // Local rails use uLocalRailColor (white, or black over light territory)
+ // instead of the palette border row.
+ vec3 railColor = owner == 0u
+ ? vec3(0.75)
+ : (owner == uint(uLocalPlayerID)
+ ? uLocalRailColor
+ : texture(uPalette, vec2((float(owner) + 0.5) / float(PALETTE_SIZE), 0.75)).rgb);
// Overlapping railroad highlight — green tint
if (highlighted) railColor = vec3(0.2, 0.85, 0.3);
if (hitBridge) {
@@ -146,9 +181,11 @@ void main() {
}
} else if (hitGhost) {
float ghostAlpha = uRailAlpha * ghostCov * 0.5;
- vec3 ghostColor = uGhostOwnerID > 0.0
- ? texture(uPalette, vec2((uGhostOwnerID + 0.5) / float(PALETTE_SIZE), 0.75)).rgb
- : vec3(0.75);
+ vec3 ghostColor = uGhostOwnerID <= 0.0
+ ? vec3(0.75)
+ : (uGhostOwnerID == uLocalPlayerID
+ ? uLocalRailColor
+ : texture(uPalette, vec2((uGhostOwnerID + 0.5) / float(PALETTE_SIZE), 0.75)).rgb);
fragColor = vec4(ghostColor, ghostAlpha * uRailFade);
} else {
fragColor = vec4(bridgeColor, uRailFade);
diff --git a/src/client/view/PlayerView.ts b/src/client/view/PlayerView.ts
index 456582098..0a34a7c96 100644
--- a/src/client/view/PlayerView.ts
+++ b/src/client/view/PlayerView.ts
@@ -107,6 +107,7 @@ export class PlayerView {
private _territoryColor: Colord;
private _borderColor: Colord;
+ private _railColor: Colord;
// Update here to include structure light and dark colors
private _structureColors: { light: Colord; dark: Colord };
@@ -173,6 +174,17 @@ export class PlayerView {
maybeFocusedBorderColor.toHex(),
);
+ // Rail color (only used for the local player's rails): white for
+ // visibility, flipped to black when the territory is too light for white
+ // to read against it. Patterns paint both colors, so average them.
+ const railBackdropBrightness = pattern
+ ? (this._territoryColor.brightness() + this._borderColor.brightness()) / 2
+ : this._territoryColor.brightness();
+ this._railColor =
+ railBackdropBrightness > 0.8
+ ? colord("rgb(0,0,0)")
+ : theme.focusedBorderColor();
+
const baseRgb = this._borderColor.toRgb();
this._borderColorNeutral = this._borderColor;
@@ -253,6 +265,10 @@ export class PlayerView {
return this._structureColors;
}
+ railColor(): Colord {
+ return this._railColor;
+ }
+
/**
* Border color for a tile:
* - Tints by neighbor relations (embargo → red, friendly → green, else neutral).
diff --git a/tests/GraphicsOverrides.test.ts b/tests/GraphicsOverrides.test.ts
index 570033f5c..a92ea9b0a 100644
--- a/tests/GraphicsOverrides.test.ts
+++ b/tests/GraphicsOverrides.test.ts
@@ -54,6 +54,18 @@ describe("GraphicsOverridesSchema", () => {
}
});
+ test("accepts partial railroad overrides", () => {
+ const cases = [
+ { railroad: {} },
+ { railroad: { railMinZoom: 2 } },
+ { railroad: { railThickness: 1.5 } },
+ { railroad: { railMinZoom: 0, railThickness: 3 } },
+ ];
+ for (const c of cases) {
+ expect(GraphicsOverridesSchema.safeParse(c).success).toBe(true);
+ }
+ });
+
test("rejects wrong field types", () => {
expect(
GraphicsOverridesSchema.safeParse({ name: { nameScaleFactor: "big" } })
@@ -72,6 +84,16 @@ describe("GraphicsOverridesSchema", () => {
mapOverlay: { territorySaturation: "full" },
}).success,
).toBe(false);
+ expect(
+ GraphicsOverridesSchema.safeParse({
+ railroad: { railMinZoom: "far" },
+ }).success,
+ ).toBe(false);
+ expect(
+ GraphicsOverridesSchema.safeParse({
+ railroad: { railThickness: "wide" },
+ }).success,
+ ).toBe(false);
});
});
@@ -214,6 +236,31 @@ describe("applyGraphicsOverrides", () => {
expect(mo.territoryDefenseDarken).toBe(defaults.territoryDefenseDarken);
});
+ test("applies railMinZoom override (including 0)", () => {
+ expect(gen({ railroad: { railMinZoom: 7 } }).railroad.railMinZoom).toBe(7);
+ expect(gen({ railroad: { railMinZoom: 0 } }).railroad.railMinZoom).toBe(0);
+ });
+
+ test("applies railThickness override (including values below 1)", () => {
+ expect(
+ gen({ railroad: { railThickness: 2.5 } }).railroad.railThickness,
+ ).toBe(2.5);
+ expect(
+ gen({ railroad: { railThickness: 0.5 } }).railroad.railThickness,
+ ).toBe(0.5);
+ });
+
+ test("railroad override leaves other railroad fields at defaults", () => {
+ const defaults = createRenderSettings().railroad;
+ const r = gen({ railroad: { railThickness: 2 } }).railroad;
+ expect(r.railMinZoom).toBe(defaults.railMinZoom);
+ expect(r.railFadeRange).toBe(defaults.railFadeRange);
+ expect(r.railDetailZoom).toBe(defaults.railDetailZoom);
+ expect(r.railAlpha).toBe(defaults.railAlpha);
+ const z = gen({ railroad: { railMinZoom: 1 } }).railroad;
+ expect(z.railThickness).toBe(defaults.railThickness);
+ });
+
test("classicIcons + name overrides compose independently", () => {
const s = gen({
name: { darkNames: true, nameScaleFactor: 0.9 },