Improve railroad visibility: own-rail contrast color and thickness setting

Local-player rails previously rendered in the white focused-border color
from the palette, making them hard to see on light territory. Rails now
use a dedicated local rail color: white normally, flipped to black when
the territory backdrop is too light for white to read against (patterns
average their primary/secondary brightness).

Also add a railThickness render setting (0.5-3, default 1), exposed in
the Graphics Settings modal and the debug GUI, and persisted via
GraphicsOverrides. In the medium-zoom LOD, rails are now drawn as
screen-space anti-aliased lines around each tile's rail centerline,
accumulated from the 3x3 neighborhood so thick lines spill cleanly into
neighboring tiles; detailed mode scales its sub-grid band widths.

- PlayerView: compute railColor() (white/black by backdrop brightness)
- RailroadPass/shader: uLocalPlayerID, uLocalRailColor, uRailThickness
- render-settings.json, RenderSettings, GraphicsOverrides,
  RenderOverrides: new railroad.railThickness knob
- GraphicsSettingsModal: "Train track thickness" slider (+ en.json keys)
- tests: schema + apply coverage for railroad overrides
This commit is contained in:
evanpelle
2026-06-10 18:56:29 -07:00
parent b0e7d04f6e
commit 9189aac687
14 changed files with 253 additions and 44 deletions
+47
View File
@@ -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 },