mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:50:45 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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`
|
||||
<div
|
||||
@@ -608,6 +625,31 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">
|
||||
${translateText("graphics_setting.rail_thickness_label")}
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${translateText("graphics_setting.rail_thickness_desc")}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min=${RAIL_THICKNESS_MIN}
|
||||
max=${RAIL_THICKNESS_MAX}
|
||||
step=${RAIL_THICKNESS_STEP}
|
||||
.value=${String(railThickness)}
|
||||
@input=${this.onRailThicknessChange}
|
||||
class="w-full border border-slate-500 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-slate-400 w-12 text-right">
|
||||
${railThickness.toFixed(1)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider mt-2"
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export const GraphicsOverridesSchema = z
|
||||
railroad: z
|
||||
.object({
|
||||
railMinZoom: z.number(),
|
||||
railThickness: z.number(),
|
||||
})
|
||||
.partial(),
|
||||
passEnabled: z
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -112,6 +112,8 @@ export interface RenderSettings {
|
||||
railFadeRange: number;
|
||||
railDetailZoom: number;
|
||||
railAlpha: number;
|
||||
/** Track width multiplier (1 = default width). */
|
||||
railThickness: number;
|
||||
};
|
||||
structure: {
|
||||
iconSize: number;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]),
|
||||
]),
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -106,7 +106,8 @@
|
||||
"railMinZoom": 4,
|
||||
"railFadeRange": 2,
|
||||
"railDetailZoom": 6,
|
||||
"railAlpha": 1
|
||||
"railAlpha": 1,
|
||||
"railThickness": 1
|
||||
},
|
||||
"structure": {
|
||||
"iconSize": 50,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user