mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:50:45 +00:00
Add Graphics Settings name color toggle and unit tests
Adds a single "Name color" toggle (Colored / Black) to the Graphics Settings modal, backed by a `darkNames` boolean in the override schema that derives the five underlying name-rendering fields (fill/outline player-color flags + static outline RGB). Forcing the outline RGB to 0 in dark mode is what makes the shader's defaultFill ramp actually render black — flipping the boolean uniforms alone wasn't enough because the fill is derived from uOutlineColor when fillUsePlayerColor is false. Flips the render-settings.json defaults so black names are the renderer baseline; the modal's no-override state follows the JSON source of truth. Adds tests covering schema parse behavior and the generateRenderSettings derivation for each override field.
This commit is contained in:
@@ -923,6 +923,10 @@
|
||||
"name_scale_label": "Name Scale",
|
||||
"name_cull_label": "Minimum name size",
|
||||
"name_cull_desc": "Hide names smaller than this size",
|
||||
"colored_names_label": "Name color",
|
||||
"colored_names_desc": "Show player names in their player color or in black",
|
||||
"colored": "Colored",
|
||||
"black": "Black",
|
||||
"reset_label": "Reset to defaults",
|
||||
"reset_desc": "Clear all graphics overrides"
|
||||
},
|
||||
|
||||
@@ -137,6 +137,17 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
this.patchName({ cullThreshold: value });
|
||||
}
|
||||
|
||||
private currentDarkNames(): boolean {
|
||||
return (
|
||||
this.userSettings.graphicsOverrides().name?.darkNames ??
|
||||
!renderDefaults.name.fillUsePlayerColor
|
||||
);
|
||||
}
|
||||
|
||||
private onToggleNamesColored() {
|
||||
this.patchName({ darkNames: !this.currentDarkNames() });
|
||||
}
|
||||
|
||||
private onResetClick() {
|
||||
this.userSettings.setGraphicsOverrides({});
|
||||
this.requestUpdate();
|
||||
@@ -147,6 +158,7 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
|
||||
const nameScale = this.currentNameScale();
|
||||
const nameCull = this.currentNameCull();
|
||||
const namesColored = !this.currentDarkNames();
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -233,6 +245,25 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
@click=${this.onToggleNamesColored}
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">
|
||||
${translateText("graphics_setting.colored_names_label")}
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${translateText("graphics_setting.colored_names_desc")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${namesColored
|
||||
? translateText("graphics_setting.colored")
|
||||
: translateText("graphics_setting.black")}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="border-t border-slate-600 pt-3 mt-4">
|
||||
<button
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
|
||||
@@ -6,6 +6,7 @@ export const GraphicsOverridesSchema = z
|
||||
.object({
|
||||
nameScaleFactor: z.number(),
|
||||
cullThreshold: z.number(),
|
||||
darkNames: z.boolean(),
|
||||
})
|
||||
.partial(),
|
||||
})
|
||||
|
||||
@@ -279,6 +279,19 @@ export function generateRenderSettings(
|
||||
if (overrides.name?.cullThreshold !== undefined) {
|
||||
settings.name.cullThreshold = overrides.name.cullThreshold;
|
||||
}
|
||||
if (overrides.name?.darkNames !== undefined) {
|
||||
const dark = overrides.name.darkNames;
|
||||
// Dark: black fill + player-colored outline. Force outline RGB to black
|
||||
// so the shader's defaultFill ramp (mix(uOutlineColor, black, fillT))
|
||||
// collapses to pure black regardless of ambient.
|
||||
// Colored: player-colored fill + white outline (defaults from JSON).
|
||||
settings.name.fillUsePlayerColor = !dark;
|
||||
settings.name.outlineUsePlayerColor = dark;
|
||||
const channel = dark ? 0 : 1;
|
||||
settings.name.outlineR = channel;
|
||||
settings.name.outlineG = channel;
|
||||
settings.name.outlineB = channel;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
||||
@@ -160,11 +160,11 @@
|
||||
"nameScaleCap": 3,
|
||||
"troopSizeMultiplier": 0.6,
|
||||
"outlineWidth": 1.4,
|
||||
"outlineR": 1.0,
|
||||
"outlineG": 1.0,
|
||||
"outlineB": 1.0,
|
||||
"outlineUsePlayerColor": false,
|
||||
"fillUsePlayerColor": true,
|
||||
"outlineR": 0.0,
|
||||
"outlineG": 0.0,
|
||||
"outlineB": 0.0,
|
||||
"outlineUsePlayerColor": true,
|
||||
"fillUsePlayerColor": false,
|
||||
"emojiRowOffset": 1.4,
|
||||
"statusRowOffset": 1.4
|
||||
},
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { GraphicsOverridesSchema } from "../src/client/render/gl/GraphicsOverrides";
|
||||
import {
|
||||
createRenderSettings,
|
||||
generateRenderSettings,
|
||||
} from "../src/client/render/gl/RenderSettings";
|
||||
|
||||
describe("GraphicsOverridesSchema", () => {
|
||||
test("accepts empty object", () => {
|
||||
expect(GraphicsOverridesSchema.safeParse({}).success).toBe(true);
|
||||
});
|
||||
|
||||
test("accepts partial name overrides", () => {
|
||||
const cases = [
|
||||
{ name: {} },
|
||||
{ name: { nameScaleFactor: 0.8 } },
|
||||
{ name: { cullThreshold: 0.02 } },
|
||||
{ name: { darkNames: true } },
|
||||
{ name: { nameScaleFactor: 1.2, cullThreshold: 0, darkNames: false } },
|
||||
];
|
||||
for (const c of cases) {
|
||||
expect(GraphicsOverridesSchema.safeParse(c).success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects wrong field types", () => {
|
||||
expect(
|
||||
GraphicsOverridesSchema.safeParse({ name: { nameScaleFactor: "big" } })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
GraphicsOverridesSchema.safeParse({ name: { darkNames: "yes" } }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateRenderSettings", () => {
|
||||
test("with empty overrides matches createRenderSettings defaults", () => {
|
||||
const fromGen = generateRenderSettings({});
|
||||
const fromCreate = createRenderSettings();
|
||||
expect(fromGen).toEqual(fromCreate);
|
||||
});
|
||||
|
||||
test("returns a fresh object each call (no shared mutation)", () => {
|
||||
const a = generateRenderSettings({});
|
||||
const b = generateRenderSettings({});
|
||||
expect(a).not.toBe(b);
|
||||
expect(a.name).not.toBe(b.name);
|
||||
a.name.nameScaleFactor = 999;
|
||||
expect(b.name.nameScaleFactor).not.toBe(999);
|
||||
});
|
||||
|
||||
test("does not mutate the overrides input", () => {
|
||||
const overrides = { name: { darkNames: true as const } };
|
||||
const snapshot = JSON.parse(JSON.stringify(overrides));
|
||||
generateRenderSettings(overrides);
|
||||
expect(overrides).toEqual(snapshot);
|
||||
});
|
||||
|
||||
test("applies nameScaleFactor override", () => {
|
||||
const settings = generateRenderSettings({ name: { nameScaleFactor: 1.3 } });
|
||||
expect(settings.name.nameScaleFactor).toBe(1.3);
|
||||
});
|
||||
|
||||
test("applies cullThreshold override (including 0)", () => {
|
||||
expect(
|
||||
generateRenderSettings({ name: { cullThreshold: 0.03 } }).name
|
||||
.cullThreshold,
|
||||
).toBe(0.03);
|
||||
expect(
|
||||
generateRenderSettings({ name: { cullThreshold: 0 } }).name.cullThreshold,
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test("darkNames=true → black fill + player-colored outline + outline RGB 0", () => {
|
||||
const s = generateRenderSettings({ name: { darkNames: true } }).name;
|
||||
expect(s.fillUsePlayerColor).toBe(false);
|
||||
expect(s.outlineUsePlayerColor).toBe(true);
|
||||
expect(s.outlineR).toBe(0);
|
||||
expect(s.outlineG).toBe(0);
|
||||
expect(s.outlineB).toBe(0);
|
||||
});
|
||||
|
||||
test("darkNames=false → player-colored fill + white outline + outline RGB 1", () => {
|
||||
const s = generateRenderSettings({ name: { darkNames: false } }).name;
|
||||
expect(s.fillUsePlayerColor).toBe(true);
|
||||
expect(s.outlineUsePlayerColor).toBe(false);
|
||||
expect(s.outlineR).toBe(1);
|
||||
expect(s.outlineG).toBe(1);
|
||||
expect(s.outlineB).toBe(1);
|
||||
});
|
||||
|
||||
test("only-darkNames override leaves nameScale/cull at defaults", () => {
|
||||
const defaults = createRenderSettings().name;
|
||||
const s = generateRenderSettings({ name: { darkNames: true } }).name;
|
||||
expect(s.nameScaleFactor).toBe(defaults.nameScaleFactor);
|
||||
expect(s.cullThreshold).toBe(defaults.cullThreshold);
|
||||
});
|
||||
|
||||
test("combined overrides all apply together", () => {
|
||||
const s = generateRenderSettings({
|
||||
name: { nameScaleFactor: 0.9, cullThreshold: 0.01, darkNames: true },
|
||||
}).name;
|
||||
expect(s.nameScaleFactor).toBe(0.9);
|
||||
expect(s.cullThreshold).toBe(0.01);
|
||||
expect(s.fillUsePlayerColor).toBe(false);
|
||||
expect(s.outlineUsePlayerColor).toBe(true);
|
||||
expect(s.outlineR).toBe(0);
|
||||
});
|
||||
|
||||
test("settings outside the name slice are untouched by name overrides", () => {
|
||||
const defaults = createRenderSettings();
|
||||
const s = generateRenderSettings({
|
||||
name: { nameScaleFactor: 0.6, darkNames: true },
|
||||
});
|
||||
expect(s.passEnabled).toEqual(defaults.passEnabled);
|
||||
expect(s.dayNight).toEqual(defaults.dayNight);
|
||||
expect(s.structure).toEqual(defaults.structure);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user