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>
This commit is contained in:
Evan
2026-06-11 12:50:50 -07:00
committed by GitHub
parent 3c0ff7a6f2
commit 1db02acdc2
20 changed files with 1329 additions and 1180 deletions
+9
View File
@@ -49,6 +49,7 @@ each frame (and animate from local time, e.g. the spawn-overlay breath).
| `gl/Camera.ts` | World↔screen math; mutated externally each frame via `setCameraState` |
| `gl/RenderSettings.ts` | Typed view of `render-settings.json` (tuning knobs) |
| `gl/render-settings.json` | All per-pass tuning constants (alpha, radii, colors, etc.) |
| `gl/*-theme.json` | Theme data (player/team palettes, color-derivation knobs) — the active one is combined into `settings.theme` at runtime |
| `gl/passes/` | One file per pass — see "Pass conventions" below |
| `gl/utils/` | Cross-pass helpers: `GlUtils` (program/shader compile), `TileCodec` (`OWNER_MASK` etc.), `NukeTrajectory` (Bezier math), `Affiliation`, `HeatManager`, `GpuResources` |
| `gl/shaders/` | `.glsl` source files (`?raw` imported by passes) |
@@ -144,6 +145,14 @@ constants. Passes read their slice (`settings.spawnOverlay`, `settings.bar`,
etc.) at construct time and use it in `draw`. The debug GUI in `gl/debug/`
gives a live-editable view of the same object during development.
Theme data (player/team palettes, color-derivation knobs) lives in sibling
theme JSONs (`gl/default-theme.json`, `gl/colorblind-theme.json`);
`createRenderSettings()` combines the active one with `render-settings.json`
into the `settings.theme` slice (the colorblind graphics override swaps the
slice in `applyGraphicsOverrides`). The theme module in `src/client/theme/`
builds its allocators and color derivations from the same theme JSONs — see
`ThemeProvider.ts`.
## Adding a new pass
1. Define any new types in `types/` if the pass needs new input shapes.
+4 -1
View File
@@ -1,5 +1,5 @@
import type { GraphicsOverrides } from "./GraphicsOverrides";
import type { RenderSettings } from "./RenderSettings";
import { createThemeSettings, type RenderSettings } from "./RenderSettings";
const DARK_AMBIENT = 0.35;
@@ -73,6 +73,9 @@ export function applyGraphicsOverrides(
settings.name.outlineB = channel;
}
if (overrides.accessibility?.colorblind === true) {
// Swap the active theme slice for the colorblind palette (replaced
// wholesale — palette arrays differ in length between themes).
settings.theme = createThemeSettings("colorblind");
// Swap the red/green friend-foe encoding (the most common confusion axis)
// for a colorblind-safe blue/orange pairing (Okabe-Ito).
// Alt-view affiliation borders: self/ally in the blue family, enemy orange.
+63 -2
View File
@@ -1,6 +1,46 @@
import colorblindTheme from "./colorblind-theme.json";
import defaultTheme from "./default-theme.json";
import defaults from "./render-settings.json";
/**
* Theme data — player/team palettes and color-derivation knobs. Loaded from a
* theme JSON file (default-theme.json or colorblind-theme.json) and combined
* with render-settings.json at runtime so all graphics configuration flows
* through one pipeline. Colors are hex strings; palettes are consumed by the
* theme module (src/client/theme/), which generates team variations and
* allocates player colors at runtime.
*/
export interface ThemeSettings {
/**
* Base color per colored team (keys match ColoredTeams). Per-player
* variations are generated at runtime; Bot stays a single flat color.
*/
teamColors: Record<string, string>;
humanColors: string[];
nationColors: string[];
botColors: string[];
/** Used when the primary palettes are exhausted. */
fallbackColors: string[];
/** Border = territory color darkened by this absolute amount. */
borderDarken: number;
/**
* Border HSL lightness multiplier, applied before borderDarken. 1 = no-op.
* Scaling keeps every border the same proportion darker than its fill
* (used by the colorblind theme so dark fills don't collapse to black).
*/
borderLightnessScale: number;
defendedBorderDarkenLight: number;
defendedBorderDarkenDark: number;
/** Minimum LAB delta between structure fill and border colors. */
structureContrastTarget: number;
/** Border color of the local player's territory. */
focusedBorderColor: string;
/** Tint applied to unit sprites during spawn highlight. */
spawnHighlightColor: string;
}
export interface RenderSettings {
theme: ThemeSettings;
passEnabled: {
terrain: boolean;
territory: boolean;
@@ -311,9 +351,30 @@ export interface RenderSettings {
lightConfigs: Record<string, { radius: number; intensity: number }>;
}
/** Create a fresh settings object with defaults from render-settings.json. */
export type ThemeName = "default" | "colorblind";
// Typed so tsc validates each theme JSON against the ThemeSettings shape.
const THEMES: Record<ThemeName, ThemeSettings> = {
default: defaultTheme,
colorblind: colorblindTheme,
};
/** Create fresh theme settings with defaults from the named theme JSON. */
export function createThemeSettings(
name: ThemeName = "default",
): ThemeSettings {
return JSON.parse(JSON.stringify(THEMES[name])) as ThemeSettings;
}
/**
* Create a fresh settings object: render-settings.json combined with the
* active theme JSON.
*/
export function createRenderSettings(): RenderSettings {
return JSON.parse(JSON.stringify(defaults)) as RenderSettings;
return {
...(JSON.parse(JSON.stringify(defaults)) as Omit<RenderSettings, "theme">),
theme: createThemeSettings(),
};
}
/** Dump current settings to a downloadable JSON file. */
+2
View File
@@ -673,6 +673,8 @@ export class GPURenderer {
);
// SAM radius pass stores its own copy
this.samRadiusPass.setPaletteData(this.paletteData);
// Name pass caches per-player colors and bakes them into slot rows
this.namePass.refreshPlayerColors(this.paletteData);
}
/** Register late-arriving players (updates palette + NamePass lookup maps). */
+10 -2
View File
@@ -4,10 +4,18 @@
type Obj = Record<string, any>;
/** Recursively assign source values onto target, preserving target's structure. */
/**
* 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 (
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" &&
+430
View File
@@ -0,0 +1,430 @@
{
"teamColors": {
"Red": "#d55e00",
"Blue": "#0072b2",
"Teal": "#009e73",
"Purple": "#cc79a7",
"Yellow": "#f0e442",
"Orange": "#e69f00",
"Green": "#56b4e9",
"Bot": "#d1cdc7",
"Humans": "#0072b2",
"Nations": "#d55e00"
},
"humanColors": [
"#b60056",
"#007700",
"#0076fb",
"#db5f11",
"#00b8ae",
"#ff75f9",
"#b6c706",
"#00e8ff",
"#c7003b",
"#008b3a",
"#7e72ff",
"#cf8400",
"#00c8ee",
"#ff75dd",
"#90e448",
"#0069e0",
"#cd331d",
"#009e79",
"#cb6af6",
"#b9a600",
"#00d4ff",
"#ff83bd",
"#007100",
"#0069ef",
"#c76000",
"#00afb8",
"#ff64e0",
"#9ac407",
"#00dcff",
"#ba0025",
"#008445",
"#8f62ef"
],
"nationColors": [
"#b60056",
"#007700",
"#0076fb",
"#db5f11",
"#00b8ae",
"#ff75f9",
"#b6c706",
"#00e8ff",
"#c7003b",
"#008b3a",
"#7e72ff",
"#cf8400",
"#00c8ee",
"#ff75dd",
"#90e448",
"#0069e0",
"#cd331d",
"#009e79",
"#cb6af6",
"#b9a600",
"#00d4ff",
"#ff83bd",
"#007100",
"#0069ef",
"#c76000",
"#00afb8",
"#ff64e0",
"#9ac407",
"#00dcff",
"#ba0025",
"#008445",
"#8f62ef"
],
"botColors": [
"#b60056",
"#007700",
"#0076fb",
"#db5f11",
"#00b8ae",
"#ff75f9",
"#b6c706",
"#00e8ff",
"#c7003b",
"#008b3a",
"#7e72ff",
"#cf8400",
"#00c8ee",
"#ff75dd",
"#90e448",
"#0069e0",
"#cd331d",
"#009e79",
"#cb6af6",
"#b9a600",
"#00d4ff",
"#ff83bd",
"#007100",
"#0069ef",
"#c76000",
"#00afb8",
"#ff64e0",
"#9ac407",
"#00dcff",
"#ba0025",
"#008445",
"#8f62ef"
],
"fallbackColors": [
"#230000",
"#2d0000",
"#370000",
"#410000",
"#4b0000",
"#550000",
"#5f0000",
"#690000",
"#730000",
"#7d0000",
"#870000",
"#910000",
"#9b0000",
"#a50000",
"#af0000",
"#b90000",
"#c30005",
"#cd000a",
"#d7000f",
"#e10014",
"#eb0019",
"#f5001e",
"#ff0023",
"#ff0a2d",
"#ff1437",
"#ff1e41",
"#ff284b",
"#ff3255",
"#ff3c5f",
"#ff4669",
"#ff5073",
"#ff5a7d",
"#ff6487",
"#ff6e91",
"#ff789b",
"#ff82a5",
"#ff8caf",
"#ff96b9",
"#ffa0c3",
"#ffaacd",
"#ffb4d7",
"#ffbee1",
"#ffc8eb",
"#002d00",
"#003700",
"#004100",
"#004b00",
"#005500",
"#005f00",
"#006900",
"#007300",
"#007d00",
"#008700",
"#009100",
"#009b00",
"#00a500",
"#00af00",
"#00b900",
"#00c305",
"#00cd0a",
"#00d70f",
"#00e114",
"#00eb19",
"#00f51e",
"#00ff23",
"#0aff2d",
"#14ff37",
"#1eff41",
"#28ff4b",
"#32ff55",
"#3cff5f",
"#46ff69",
"#50ff73",
"#5aff7d",
"#64ff87",
"#6eff91",
"#78ff9b",
"#82ffa5",
"#8cffaf",
"#96ffb9",
"#a0ffc3",
"#aaffcd",
"#b4ffd7",
"#beffe1",
"#c8ffeb",
"#000023",
"#00002d",
"#000037",
"#000041",
"#00004b",
"#000055",
"#00005f",
"#000069",
"#000073",
"#00007d",
"#000087",
"#000091",
"#00009b",
"#0000a5",
"#0000af",
"#0000b9",
"#0500c3",
"#0a00cd",
"#0f00d7",
"#1400e1",
"#1900eb",
"#1e00f5",
"#2300ff",
"#2d0aff",
"#3714ff",
"#411eff",
"#4b28ff",
"#5532ff",
"#5f3cff",
"#6946ff",
"#7350ff",
"#7d5aff",
"#8764ff",
"#916eff",
"#9b78ff",
"#a582ff",
"#af8cff",
"#b996ff",
"#c3a0ff",
"#cdaaff",
"#d7b4ff",
"#e1beff",
"#ebc8ff",
"#230023",
"#2d002d",
"#370037",
"#410041",
"#4b004b",
"#550055",
"#5f005f",
"#690069",
"#730073",
"#7d007d",
"#870087",
"#910091",
"#9b009b",
"#a500a5",
"#af00af",
"#b900b9",
"#c305c3",
"#cd0acd",
"#d70fd7",
"#e114e1",
"#eb19eb",
"#f51ef5",
"#ff23ff",
"#ff2dff",
"#ff37ff",
"#ff41ff",
"#ff4bff",
"#ff55ff",
"#ff5fff",
"#ff69ff",
"#ff73ff",
"#ff7dff",
"#ff87ff",
"#ff91ff",
"#ff9bff",
"#ffa5ff",
"#ffafff",
"#ffb9ff",
"#ffc3ff",
"#ffcdff",
"#ffd7ff",
"#002323",
"#002d2d",
"#003737",
"#004141",
"#004b4b",
"#005555",
"#005f5f",
"#006969",
"#007373",
"#007d7d",
"#008787",
"#009191",
"#009b9b",
"#00a5a5",
"#00afaf",
"#00b9b9",
"#05c3c3",
"#0acdcd",
"#0fd7d7",
"#14e1e1",
"#19ebeb",
"#1ef5f5",
"#23ffff",
"#2dffff",
"#37ffff",
"#41ffff",
"#4bffff",
"#55ffff",
"#5fffff",
"#69ffff",
"#73ffff",
"#7dffff",
"#87ffff",
"#91ffff",
"#9bffff",
"#a5ffff",
"#afffff",
"#b9ffff",
"#c3ffff",
"#cdffff",
"#d7ffff",
"#232300",
"#2d2d00",
"#373700",
"#414100",
"#4b4b00",
"#555500",
"#5f5f00",
"#696900",
"#737300",
"#7d7d00",
"#878700",
"#919100",
"#9b9b00",
"#a5a500",
"#afaf00",
"#b9b900",
"#c3c305",
"#cdcd0a",
"#d7d70f",
"#e1e114",
"#ebeb19",
"#f5f51e",
"#ffff23",
"#ffff2d",
"#ffff37",
"#ffff41",
"#ffff4b",
"#ffff55",
"#ffff5f",
"#ffff69",
"#ffff73",
"#ffff7d",
"#ffff87",
"#ffff91",
"#ffff9b",
"#ffffa5",
"#ffffaf",
"#ffffb9",
"#ffffc3",
"#ffffcd",
"#ffffd7",
"#d7ffc8",
"#e1ffaf",
"#f0faa0",
"#f5f5af",
"#96c8ff",
"#a0d7ff",
"#aae1ff",
"#b4ebfa",
"#bef5f0",
"#d2fff5",
"#dcffff",
"#e6faff",
"#f0f0ff",
"#fae6ff",
"#aabeff",
"#b4b4ff",
"#c8aaff",
"#be8cc3",
"#c391c8",
"#c896cd",
"#cd9bd2",
"#d2a0d7",
"#d7a5dc",
"#dcaae1",
"#e1afe6",
"#e6b4eb",
"#ebb9f0",
"#f0bef5",
"#f5c3fa",
"#fac8ff",
"#ffcdff",
"#ffd2ff",
"#ffd2fa",
"#ffcdf5",
"#ffd7f5",
"#dca0ff",
"#eb96ff",
"#f5a0f0",
"#ffaae1",
"#ffb9d7",
"#ffc3eb",
"#ffc8dc",
"#ffd2e6",
"#ffdceb",
"#ffdcfa",
"#ffe1ff",
"#ffe6f5",
"#ffebeb",
"#ffd7c3",
"#ffe1b4",
"#ffe6be",
"#ffebc8",
"#fff5d2",
"#fff0dc"
],
"borderDarken": 0,
"borderLightnessScale": 0.6,
"defendedBorderDarkenLight": 0.2,
"defendedBorderDarkenDark": 0.4,
"structureContrastTarget": 0.5,
"focusedBorderColor": "#e6e6e6",
"spawnHighlightColor": "#ffd54f"
}
+495
View File
@@ -0,0 +1,495 @@
{
"teamColors": {
"Red": "#eb3333",
"Blue": "#2962ff",
"Teal": "#2bd4bd",
"Purple": "#9234ea",
"Yellow": "#e7b008",
"Orange": "#f97415",
"Green": "#41be52",
"Bot": "#d1cdc7",
"Humans": "#2962ff",
"Nations": "#eb3333"
},
"humanColors": [
"#a3e635",
"#84cc16",
"#10b981",
"#34d399",
"#2dd4bf",
"#4ade80",
"#6ee7b7",
"#86efac",
"#97ffbb",
"#baffc9",
"#e6fad2",
"#22c55e",
"#43be54",
"#52b788",
"#30b2b4",
"#e6fffa",
"#dcf0fa",
"#e9d5ff",
"#ccccff",
"#dcdcff",
"#cae1ff",
"#93c5fd",
"#7dd3fc",
"#63cafd",
"#38bdf8",
"#60a5fa",
"#3b82f6",
"#4f46e5",
"#7c3aed",
"#9333ea",
"#b388ff",
"#a78bfa",
"#d946ef",
"#a855f7",
"#be5cfb",
"#c084fc",
"#f0abfc",
"#f472b6",
"#ec4899",
"#dc2626",
"#ef4444",
"#eb4b4b",
"#f56565",
"#f87171",
"#fb7185",
"#fda4af",
"#fca5a5",
"#ffcce5",
"#fad7e1",
"#fbebf5",
"#f0f0c8",
"#fafad2",
"#fff0c8",
"#ffdfba",
"#fcd34d",
"#fbbf24",
"#eab308",
"#ca8a04",
"#f59e0b",
"#fb923c",
"#f97316",
"#ea580c",
"#854d0e"
],
"nationColors": [
"#d2d264",
"#b4d278",
"#aabe64",
"#50c878",
"#82c882",
"#8cb48c",
"#a0bea0",
"#a0b48c",
"#64a050",
"#648c6e",
"#64b4a0",
"#82b4aa",
"#aabeb4",
"#648296",
"#78a0c8",
"#8c96b4",
"#64d2d2",
"#8cb4dc",
"#82aabe",
"#64b4e6",
"#5082be",
"#7878be",
"#966ebe",
"#a078a0",
"#aa8cbe",
"#b482b4",
"#be8c96",
"#b464e6",
"#b4a0b4",
"#aa96aa",
"#968296",
"#e6b4b4",
"#d2a0c8",
"#e682b4",
"#d264a0",
"#be6482",
"#dc7878",
"#c8826e",
"#e68c8c",
"#e66464",
"#e69664",
"#d28c50",
"#e6b450",
"#c8a06e",
"#be9682",
"#beb4a0",
"#b4aa8c",
"#c8c88c",
"#beaa64"
],
"botColors": [
"#96a08c",
"#a0a096",
"#aaaa8c",
"#aaaa78",
"#96a078",
"#96aa82",
"#96aa96",
"#82aa82",
"#8ca08c",
"#789664",
"#788c78",
"#64aa82",
"#78a096",
"#82a096",
"#78aaaa",
"#78a0be",
"#8296aa",
"#8296a0",
"#8c96a0",
"#8ca0aa",
"#96a0a0",
"#6478a0",
"#78828c",
"#8282a0",
"#8c828c",
"#8c78a0",
"#968296",
"#968ca0",
"#a082a0",
"#aa96aa",
"#a078be",
"#a07882",
"#aa788c",
"#aa8278",
"#aa8282",
"#b48c8c",
"#be82a0",
"#be7878",
"#be8c78",
"#bea064",
"#aa8c64",
"#a08c82",
"#aa9682",
"#a09678",
"#a0968c",
"#a08c96",
"#a096a0",
"#968c96",
"#b4a0a0"
],
"fallbackColors": [
"#230000",
"#2d0000",
"#370000",
"#410000",
"#4b0000",
"#550000",
"#5f0000",
"#690000",
"#730000",
"#7d0000",
"#870000",
"#910000",
"#9b0000",
"#a50000",
"#af0000",
"#b90000",
"#c30005",
"#cd000a",
"#d7000f",
"#e10014",
"#eb0019",
"#f5001e",
"#ff0023",
"#ff0a2d",
"#ff1437",
"#ff1e41",
"#ff284b",
"#ff3255",
"#ff3c5f",
"#ff4669",
"#ff5073",
"#ff5a7d",
"#ff6487",
"#ff6e91",
"#ff789b",
"#ff82a5",
"#ff8caf",
"#ff96b9",
"#ffa0c3",
"#ffaacd",
"#ffb4d7",
"#ffbee1",
"#ffc8eb",
"#002d00",
"#003700",
"#004100",
"#004b00",
"#005500",
"#005f00",
"#006900",
"#007300",
"#007d00",
"#008700",
"#009100",
"#009b00",
"#00a500",
"#00af00",
"#00b900",
"#00c305",
"#00cd0a",
"#00d70f",
"#00e114",
"#00eb19",
"#00f51e",
"#00ff23",
"#0aff2d",
"#14ff37",
"#1eff41",
"#28ff4b",
"#32ff55",
"#3cff5f",
"#46ff69",
"#50ff73",
"#5aff7d",
"#64ff87",
"#6eff91",
"#78ff9b",
"#82ffa5",
"#8cffaf",
"#96ffb9",
"#a0ffc3",
"#aaffcd",
"#b4ffd7",
"#beffe1",
"#c8ffeb",
"#000023",
"#00002d",
"#000037",
"#000041",
"#00004b",
"#000055",
"#00005f",
"#000069",
"#000073",
"#00007d",
"#000087",
"#000091",
"#00009b",
"#0000a5",
"#0000af",
"#0000b9",
"#0500c3",
"#0a00cd",
"#0f00d7",
"#1400e1",
"#1900eb",
"#1e00f5",
"#2300ff",
"#2d0aff",
"#3714ff",
"#411eff",
"#4b28ff",
"#5532ff",
"#5f3cff",
"#6946ff",
"#7350ff",
"#7d5aff",
"#8764ff",
"#916eff",
"#9b78ff",
"#a582ff",
"#af8cff",
"#b996ff",
"#c3a0ff",
"#cdaaff",
"#d7b4ff",
"#e1beff",
"#ebc8ff",
"#230023",
"#2d002d",
"#370037",
"#410041",
"#4b004b",
"#550055",
"#5f005f",
"#690069",
"#730073",
"#7d007d",
"#870087",
"#910091",
"#9b009b",
"#a500a5",
"#af00af",
"#b900b9",
"#c305c3",
"#cd0acd",
"#d70fd7",
"#e114e1",
"#eb19eb",
"#f51ef5",
"#ff23ff",
"#ff2dff",
"#ff37ff",
"#ff41ff",
"#ff4bff",
"#ff55ff",
"#ff5fff",
"#ff69ff",
"#ff73ff",
"#ff7dff",
"#ff87ff",
"#ff91ff",
"#ff9bff",
"#ffa5ff",
"#ffafff",
"#ffb9ff",
"#ffc3ff",
"#ffcdff",
"#ffd7ff",
"#002323",
"#002d2d",
"#003737",
"#004141",
"#004b4b",
"#005555",
"#005f5f",
"#006969",
"#007373",
"#007d7d",
"#008787",
"#009191",
"#009b9b",
"#00a5a5",
"#00afaf",
"#00b9b9",
"#05c3c3",
"#0acdcd",
"#0fd7d7",
"#14e1e1",
"#19ebeb",
"#1ef5f5",
"#23ffff",
"#2dffff",
"#37ffff",
"#41ffff",
"#4bffff",
"#55ffff",
"#5fffff",
"#69ffff",
"#73ffff",
"#7dffff",
"#87ffff",
"#91ffff",
"#9bffff",
"#a5ffff",
"#afffff",
"#b9ffff",
"#c3ffff",
"#cdffff",
"#d7ffff",
"#232300",
"#2d2d00",
"#373700",
"#414100",
"#4b4b00",
"#555500",
"#5f5f00",
"#696900",
"#737300",
"#7d7d00",
"#878700",
"#919100",
"#9b9b00",
"#a5a500",
"#afaf00",
"#b9b900",
"#c3c305",
"#cdcd0a",
"#d7d70f",
"#e1e114",
"#ebeb19",
"#f5f51e",
"#ffff23",
"#ffff2d",
"#ffff37",
"#ffff41",
"#ffff4b",
"#ffff55",
"#ffff5f",
"#ffff69",
"#ffff73",
"#ffff7d",
"#ffff87",
"#ffff91",
"#ffff9b",
"#ffffa5",
"#ffffaf",
"#ffffb9",
"#ffffc3",
"#ffffcd",
"#ffffd7",
"#d7ffc8",
"#e1ffaf",
"#f0faa0",
"#f5f5af",
"#96c8ff",
"#a0d7ff",
"#aae1ff",
"#b4ebfa",
"#bef5f0",
"#d2fff5",
"#dcffff",
"#e6faff",
"#f0f0ff",
"#fae6ff",
"#aabeff",
"#b4b4ff",
"#c8aaff",
"#be8cc3",
"#c391c8",
"#c896cd",
"#cd9bd2",
"#d2a0d7",
"#d7a5dc",
"#dcaae1",
"#e1afe6",
"#e6b4eb",
"#ebb9f0",
"#f0bef5",
"#f5c3fa",
"#fac8ff",
"#ffcdff",
"#ffd2ff",
"#ffd2fa",
"#ffcdf5",
"#ffd7f5",
"#dca0ff",
"#eb96ff",
"#f5a0f0",
"#ffaae1",
"#ffb9d7",
"#ffc3eb",
"#ffc8dc",
"#ffd2e6",
"#ffdceb",
"#ffdcfa",
"#ffe1ff",
"#ffe6f5",
"#ffebeb",
"#ffd7c3",
"#ffe1b4",
"#ffe6be",
"#ffebc8",
"#fff5d2",
"#fff0dc"
],
"borderDarken": 0.125,
"borderLightnessScale": 1,
"defendedBorderDarkenLight": 0.2,
"defendedBorderDarkenDark": 0.4,
"structureContrastTarget": 0.5,
"focusedBorderColor": "#e6e6e6",
"spawnHighlightColor": "#ffd54f"
}
@@ -222,6 +222,25 @@ export class NamePass {
}
}
/**
* Re-read every known player's territory color from the palette and rewrite
* the live slot rows. Called after a mid-game palette refresh (e.g. toggling
* colorblind mode) so name fills/outlines pick up the re-themed colors.
*/
refreshPlayerColors(paletteData: Float32Array): void {
for (const [id, p] of this.playerByID) {
const off = p.smallID * 4;
this.playerColors.set(id, [
paletteData[off],
paletteData[off + 1],
paletteData[off + 2],
]);
}
for (const slot of this.slots.values()) {
this.writePlayerDataRow(slot);
}
}
/**
* Request the texture layer for a slot's flag (called once at slot creation).
* If the image is already loaded the layer index is set immediately; otherwise
+2 -2
View File
@@ -2,7 +2,7 @@
* GPU-ready color utilities.
*
* Terrain RGBA: Uint8Array(w × h × 4) — one RGBA pixel per tile, computed
* from PastelTheme rules applied to the raw terrain byte layout.
* from the terrain color rules applied to the raw terrain byte layout.
*
* Player palette is NOT built here — consumers provide a pre-built
* Float32Array(PALETTE_SIZE × 2 × 4) to the GPURenderer constructor.
@@ -19,7 +19,7 @@ export function getPaletteSize(): number {
/**
* Compute a static RGBA8 texture from raw terrain bytes.
* Replicates PastelTheme.terrainColor() on the CPU.
* The single source of truth for terrain colors.
*
* Terrain byte layout per tile:
* bit 7: isLand