Files
OpenFrontIO/src/client/render/gl/SettingsUtils.ts
T
Evan 1db02acdc2 Move theme data into the render-settings JSON pipeline (#4223)
**Add approved & assigned issue number here:**

N/A — maintainer refactor.

## Description:

Replaces the theme class hierarchy
(`BaseTheme`/`PastelTheme`/`ColorblindTheme`) with theme JSON files —
`default-theme.json` and `colorblind-theme.json` — combined with
`render-settings.json` at runtime into a single graphics-configuration
pipeline (`settings.theme`). One `SettingsTheme` class keeps the
algorithms (color allocation, team-variation generation, LAB-contrast
structure colors) and reads all data from `ThemeSettings`; adding a
theme is now just adding a JSON file.

Colorblind mode (#4150) is fully preserved:

- Same palettes — the 32-color CVD-safe pool and Okabe-Ito team colors
are baked into `colorblind-theme.json`
- The relative border rule (`l × 0.6`) is expressed as a
`borderLightnessScale` knob alongside the default theme's absolute
`borderDarken`
- The mid-game re-theme wiring (`refreshPlayerColors`/`refreshPalette`)
and the affiliation/friend-foe tint overrides are unchanged;
`applyGraphicsOverrides` now also swaps the `settings.theme` slice
- `deepAssign` replaces arrays wholesale so differing palette lengths
survive theme switches

Verified against the previous implementation with an equivalence test
(since removed): default-theme colors are byte-identical including
allocation order; colorblind team/derived colors are byte-identical, and
FFA assignment may permute within the same palette (hex baking rounds
upstream's fractional-RGB colord objects, which can flip the allocator's
greedy delta-E ordering — rendered colors round identically either way).

Also removes dead theme surface (`terrainColor`, `backgroundColor`,
`falloutColor`, `font`, `textColor`, spawn-highlight variants,
`PastelThemeDark`) — GL terrain colors and dark mode were already
handled in the renderer. Note this means the colorblind terrain bands
from #4150 were dead code (nothing calls `terrainColor`; GL terrain
comes from `ColorUtils.encodeTerrainTile`); wiring CVD-safe terrain into
the terrain texture would be a follow-up.

## Please complete the following:

- [x] I have added screenshots for all UI updates — N/A, no UI changes
(verified color-identical)
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file — N/A, no user-visible text
- [x] I have added relevant tests to the test directory —
`tests/Colors.test.ts` updated for the new pipeline (team colors from
theme JSON, colorblind palette/border tests)

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evanpelle

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:50:50 -07:00

58 lines
1.5 KiB
TypeScript

/**
* Utilities for RenderSettings persistence — deep-assign, deep-diff.
*/
type Obj = Record<string, any>;
/**
* Recursively assign source values onto target, preserving target's structure.
* Arrays are replaced wholesale (theme palettes differ in length between
* themes, so per-index merging would leave stale entries behind).
*/
export function deepAssign(target: Obj, source: Obj): void {
for (const key of Object.keys(source)) {
if (Array.isArray(source[key])) {
if (key in target) {
target[key] = structuredClone(source[key]);
}
} else if (
typeof source[key] === "object" &&
source[key] !== null &&
typeof target[key] === "object" &&
target[key] !== null
) {
deepAssign(target[key] as Obj, source[key] as Obj);
} else if (key in target) {
target[key] = source[key];
}
}
}
/**
* Compute a sparse deep-partial of values that differ from defaults.
* Returns `undefined` if nothing differs.
*/
export function deepDiff(defaults: Obj, current: Obj): Obj | undefined {
let result: Obj | undefined;
for (const key of Object.keys(defaults)) {
const dv = defaults[key];
const cv = current[key];
if (
typeof dv === "object" &&
dv !== null &&
typeof cv === "object" &&
cv !== null
) {
const sub = deepDiff(dv as Obj, cv as Obj);
if (sub !== undefined) {
result ??= {};
result[key] = sub;
}
} else if (dv !== cv) {
result ??= {};
result[key] = cv;
}
}
return result;
}