From 21776e81afa15d16e54ebc2cbbbe1850a6950e7b Mon Sep 17 00:00:00 2001 From: noahschmal <131297075+noahschmal@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:53:03 -0700 Subject: [PATCH] Feature/colorblind mode (#4150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Add approved & assigned issue number here:** Resolves #2549 ## Description: Adds colorblind mode. Similar to dark mode, it exists as a toggle in settings. When enabled, it swaps the game's theme (which is refactored to extend from a theme base class) to use more colorblind-friendly colors and brightness variations. Borders are darkened, and terrarin is separated by lightness. Friendly/Foe colors and switched to blue/orange instead of red/green. The theme refactor supports adding new themes without having to reimplement the color distribution system. New themes can extend the BaseTheme and supply the data, such as palettes, team-color variations, and terrain. New setting: Screenshot 2026-06-04 at 11 30 27 AM New color palette: Screenshot 2026-06-04 at 11 30
59 AM ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: jetaviz --- resources/lang/en.json | 3 + src/client/ClientGameRunner.ts | 12 +- src/client/UserSettingModal.ts | 28 ++ src/client/WebGLFrameBuilder.ts | 13 + .../hud/layers/GraphicsSettingsModal.ts | 50 ++++ src/client/render/gl/GraphicsOverrides.ts | 5 + src/client/render/gl/RenderOverrides.ts | 33 +++ src/client/theme/BaseTheme.ts | 228 +++++++++++++++++ src/client/theme/ColorAllocator.ts | 96 ++----- src/client/theme/ColorblindTheme.ts | 113 +++++++++ src/client/theme/Colors.ts | 35 +++ src/client/theme/PastelTheme.ts | 240 ++++++------------ src/client/theme/ThemeProvider.ts | 8 +- src/client/view/GameView.ts | 11 + src/client/view/PlayerView.ts | 48 ++-- tests/Colors.test.ts | 104 ++++---- 16 files changed, 723 insertions(+), 304 deletions(-) create mode 100644 src/client/theme/BaseTheme.ts create mode 100644 src/client/theme/ColorblindTheme.ts diff --git a/resources/lang/en.json b/resources/lang/en.json index c53716615..3ccb081a6 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -810,6 +810,8 @@ "keybinds_hint": "Click a key to rebind it. You can assign a single key or Shift + key combination.", "dark_mode_label": "Dark Mode", "dark_mode_desc": "Toggle the site’s appearance between light and dark themes", + "colorblind_label": "Colorblind Mode", + "colorblind_desc": "Use colorblind-friendly territory and border colors", "emojis_label": "Emojis", "emojis_desc": "Toggle whether emojis are shown in game", "alert_frame_label": "Alert Frame", @@ -946,6 +948,7 @@ "colored": "Colored", "black": "Black", "section_structure_icons": "Structure Icons", + "section_accessibility": "Accessibility", "classic_icons_label": "Classic icons", "classic_icons_desc": "Lighter outline with near-black interior", "section_map": "Map", diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index eb806dccf..b25a05396 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -497,10 +497,20 @@ async function createClientGame( applyGraphicsOverrides(live, userSettings.graphicsOverrides()); applyDarkModeOverride(live, userSettings.darkMode()); }; + // Re-apply render settings, then re-theme and recolor players, on a + // graphics-override change (covers a theme switch such as colorblind mode). + const onGraphicsChanged = (): void => { + regenerateRenderSettings(); + // A graphics override can switch the active theme (e.g. colorblind mode), + // so re-theme existing players and re-upload the palette to recolor their + // territory fills/borders live. + gameView.refreshPlayerColors(); + webglBuilder.refreshPalette(gameView); + }; regenerateRenderSettings(); globalThis.addEventListener( `${USER_SETTINGS_CHANGED_EVENT}:${GRAPHICS_KEY}`, - regenerateRenderSettings, + onGraphicsChanged, { signal: graphicsListenerAbort.signal }, ); globalThis.addEventListener( diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index fe52b0a02..13f863bf1 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -206,6 +206,25 @@ export class UserSettingModal extends BaseModal { console.log("🌙 Dark Mode:", this.userSettings.darkMode() ? "ON" : "OFF"); } + /** Whether colorblind mode is currently enabled in the graphics overrides. */ + private colorblindMode(): boolean { + return ( + this.userSettings.graphicsOverrides().accessibility?.colorblind ?? false + ); + } + + /** Flip the colorblind-mode graphics override and persist it. */ + private toggleColorblindMode() { + const overrides = this.userSettings.graphicsOverrides(); + this.userSettings.setGraphicsOverrides({ + ...overrides, + accessibility: { + ...overrides.accessibility, + colorblind: !this.colorblindMode(), + }, + }); + } + private toggleEmojis() { this.userSettings.toggleEmojis(); @@ -742,6 +761,15 @@ export class UserSettingModal extends BaseModal { @change=${this.toggleDarkMode} > + + + , + ) { + const current = this.userSettings.graphicsOverrides(); + this.userSettings.setGraphicsOverrides({ + ...current, + accessibility: { ...current.accessibility, ...patch }, + }); + this.requestUpdate(); + } + private currentSpecialEffects(): boolean { return ( this.userSettings.graphicsOverrides().passEnabled?.fx ?? @@ -310,6 +322,18 @@ export class GraphicsSettingsModal extends LitElement implements Controller { this.patchPassEnabled({ fx: !this.currentSpecialEffects() }); } + /** Whether colorblind mode is currently enabled. */ + private currentColorblind(): boolean { + return ( + this.userSettings.graphicsOverrides().accessibility?.colorblind ?? false + ); + } + + /** Toggle colorblind-friendly colors. */ + private onToggleColorblind() { + this.patchAccessibility({ colorblind: !this.currentColorblind() }); + } + private onNameScaleChange(event: Event) { const value = parseFloat((event.target as HTMLInputElement).value); this.patchName({ nameScaleFactor: value }); @@ -356,6 +380,7 @@ export class GraphicsSettingsModal extends LitElement implements Controller { const territoryAlpha = this.currentTerritoryAlpha(); const railDrawDistance = RAIL_ZOOM_MAX - this.currentRailMinZoom(); const railThickness = this.currentRailThickness(); + const colorblind = this.currentColorblind(); return html`
+
+ ${translateText("graphics_setting.section_accessibility")} +
+ + +