diff --git a/resources/lang/en.json b/resources/lang/en.json index c11ce34d9..338cea89a 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -306,6 +306,8 @@ "tab_keybinds": "Keybinds", "dark_mode_label": "Dark Mode", "dark_mode_desc": "Toggle the site’s appearance between light and dark themes", + "user_setting.colorblind_mode_label": "Colorblind Mode", + "user_setting.colorblind_mode_desc": "Adjusts colors for red-green color blindness.", "emojis_label": "Emojis", "emojis_desc": "Toggle whether emojis are shown in game", "alert_frame_label": "Alert Frame", diff --git a/src/client/graphics/layers/SettingsModal.ts b/src/client/graphics/layers/SettingsModal.ts index 3956204f0..38c3837fa 100644 --- a/src/client/graphics/layers/SettingsModal.ts +++ b/src/client/graphics/layers/SettingsModal.ts @@ -136,6 +136,12 @@ export class SettingsModal extends LitElement implements Layer { this.requestUpdate(); } + private onToggleColorblindModeButtonClick() { + this.userSettings.toggleColorblindMode(); + this.eventBus.emit(new RefreshGraphicsEvent()); + this.requestUpdate(); + } + private onToggleRandomNameModeButtonClick() { this.userSettings.toggleRandomName(); this.requestUpdate(); @@ -321,6 +327,31 @@ export class SettingsModal extends LitElement implements Layer { + + + + + ${translateText("user_setting.colorblind_mode_label")} + + + ${translateText("user_setting.colorblind_mode_desc")} + + + + ${this.userSettings.colorblindMode() + ? translateText("user_setting.on") + : translateText("user_setting.off")} + + + (); private teamPlayerColors = new Map(); - constructor(colors: Colord[], fallback: Colord[]) { + constructor( + colors: Colord[], + fallback: Colord[], + private userSettings: UserSettings, + ) { this.availableColors = [...colors]; this.fallbackColors = [...colors, ...fallback]; } private getTeamColorVariations(team: Team): Colord[] { + const isColorblind = this.userSettings.colorblindMode(); switch (team) { case ColoredTeams.Blue: return blueTeamColors; case ColoredTeams.Red: - return redTeamColors; + return isColorblind ? generateTeamColors(redColorblind) : redTeamColors; case ColoredTeams.Teal: return tealTeamColors; case ColoredTeams.Purple: @@ -44,7 +53,9 @@ export class ColorAllocator { case ColoredTeams.Orange: return orangeTeamColors; case ColoredTeams.Green: - return greenTeamColors; + return isColorblind + ? generateTeamColors(greenColorblind) + : greenTeamColors; case ColoredTeams.Bot: return botTeamColors; default: diff --git a/src/core/configuration/Colors.ts b/src/core/configuration/Colors.ts index 2e913a6b6..f31e86fb5 100644 --- a/src/core/configuration/Colors.ts +++ b/src/core/configuration/Colors.ts @@ -14,6 +14,9 @@ export const orange = colord({ h: 25, s: 95, l: 53 }); export const green = colord({ h: 128, s: 49, l: 50 }); export const botColor = colord({ h: 36, s: 10, l: 80 }); +export const redColorblind = colord({ h: 30, s: 100, l: 50 }); // Orange +export const greenColorblind = colord({ h: 210, s: 100, l: 50 }); // Blue + export const redTeamColors: Colord[] = generateTeamColors(red); export const blueTeamColors: Colord[] = generateTeamColors(blue); export const tealTeamColors: Colord[] = generateTeamColors(teal); @@ -23,7 +26,7 @@ export const orangeTeamColors: Colord[] = generateTeamColors(orange); export const greenTeamColors: Colord[] = generateTeamColors(green); export const botTeamColors: Colord[] = [colord(botColor)]; -function generateTeamColors(baseColor: Colord): Colord[] { +export function generateTeamColors(baseColor: Colord): Colord[] { const { h: baseHue, s: baseSaturation, l: baseLightness } = baseColor.toHsl(); const colorCount = 64; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index ee7b8d1ab..89f7daaed 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -220,14 +220,17 @@ export abstract class DefaultServerConfig implements ServerConfig { } export class DefaultConfig implements Config { - private pastelTheme: PastelTheme = new PastelTheme(); - private pastelThemeDark: PastelThemeDark = new PastelThemeDark(); + private pastelTheme: PastelTheme; + private pastelThemeDark: PastelThemeDark; constructor( private _serverConfig: ServerConfig, private _gameConfig: GameConfig, private _userSettings: UserSettings | null, private _isReplay: boolean, - ) {} + ) { + this.pastelTheme = new PastelTheme(this.userSettings()); + this.pastelThemeDark = new PastelThemeDark(this.userSettings()); + } stripePublishableKey(): string { return process.env.STRIPE_PUBLISHABLE_KEY ?? ""; diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts index 627c0cb10..cda3d8bc2 100644 --- a/src/core/configuration/PastelTheme.ts +++ b/src/core/configuration/PastelTheme.ts @@ -3,6 +3,7 @@ import { PseudoRandom } from "../PseudoRandom"; import { PlayerType, Team, TerrainType } from "../game/Game"; import { GameMap, TileRef } from "../game/GameMap"; import { PlayerView } from "../game/GameView"; +import { UserSettings } from "../game/UserSettings"; import { ColorAllocator } from "./ColorAllocator"; import { botColors, fallbackColors, humanColors, nationColors } from "./Colors"; import { Theme } from "./Config"; @@ -12,10 +13,33 @@ type ColorCache = Map; export class PastelTheme implements Theme { private borderColorCache: ColorCache = new Map(); private rand = new PseudoRandom(123); - private humanColorAllocator = new ColorAllocator(humanColors, fallbackColors); - private botColorAllocator = new ColorAllocator(botColors, botColors); - private teamColorAllocator = new ColorAllocator(humanColors, fallbackColors); - private nationColorAllocator = new ColorAllocator(nationColors, nationColors); + private humanColorAllocator: ColorAllocator; + private botColorAllocator: ColorAllocator; + private teamColorAllocator: ColorAllocator; + private nationColorAllocator: ColorAllocator; + + constructor(private userSettings: UserSettings) { + this.humanColorAllocator = new ColorAllocator( + humanColors, + fallbackColors, + userSettings, + ); + this.botColorAllocator = new ColorAllocator( + botColors, + botColors, + userSettings, + ); + this.teamColorAllocator = new ColorAllocator( + humanColors, + fallbackColors, + userSettings, + ); + this.nationColorAllocator = new ColorAllocator( + nationColors, + nationColors, + userSettings, + ); + } private background = colord({ r: 60, g: 60, b: 60 }); private shore = colord({ r: 204, g: 203, b: 158 }); diff --git a/src/core/configuration/PastelThemeDark.ts b/src/core/configuration/PastelThemeDark.ts index eee5e33d6..3f27564ae 100644 --- a/src/core/configuration/PastelThemeDark.ts +++ b/src/core/configuration/PastelThemeDark.ts @@ -1,6 +1,7 @@ import { Colord, colord } from "colord"; import { TerrainType } from "../game/Game"; import { GameMap, TileRef } from "../game/GameMap"; +import { UserSettings } from "../game/UserSettings"; import { PastelTheme } from "./PastelTheme"; export class PastelThemeDark extends PastelTheme { @@ -9,6 +10,10 @@ export class PastelThemeDark extends PastelTheme { private darkWater = colord({ r: 14, g: 11, b: 30 }); private darkShorelineWater = colord({ r: 50, g: 50, b: 50 }); + constructor(userSettings: UserSettings) { + super(userSettings); + } + terrainColor(gm: GameMap, tile: TileRef): Colord { const mag = gm.magnitude(tile); if (gm.isShore(tile)) { diff --git a/src/core/game/UserSettings.ts b/src/core/game/UserSettings.ts index fd5ac12a5..817484b5c 100644 --- a/src/core/game/UserSettings.ts +++ b/src/core/game/UserSettings.ts @@ -128,6 +128,14 @@ export class UserSettings { } } + colorblindMode() { + return this.get("settings.colorblindMode", false); + } + + toggleColorblindMode() { + this.set("settings.colorblindMode", !this.colorblindMode()); + } + // For development only. Used for testing patterns, set in the console manually. getDevOnlyPattern(): PlayerPattern | undefined { const data = localStorage.getItem("dev-pattern") ?? undefined; diff --git a/tests/Colors.test.ts b/tests/Colors.test.ts index bbeccde57..02810c675 100644 --- a/tests/Colors.test.ts +++ b/tests/Colors.test.ts @@ -14,6 +14,7 @@ import { yellow, } from "../src/core/configuration/Colors"; import { ColoredTeams } from "../src/core/game/Game"; +import { UserSettings } from "../src/core/game/UserSettings"; const mockColors: Colord[] = [ colord({ r: 255, g: 0, b: 0 }), @@ -28,11 +29,19 @@ const fallbackMockColors: Colord[] = [ const fallbackColors = [...fallbackMockColors, ...mockColors]; +const mockUserSettings = { + colorblindMode: () => false, +} as UserSettings; + describe("ColorAllocator", () => { let allocator: ColorAllocator; beforeEach(() => { - allocator = new ColorAllocator(mockColors, fallbackMockColors); + allocator = new ColorAllocator( + mockColors, + fallbackMockColors, + mockUserSettings, + ); }); test("returns a unique color for each new ID", () => { @@ -67,7 +76,11 @@ describe("ColorAllocator", () => { }); test("assignBotColor returns deterministic color from botColors", () => { - const allocator = new ColorAllocator(mockColors, mockColors); + const allocator = new ColorAllocator( + mockColors, + mockColors, + mockUserSettings, + ); const id1 = "bot123"; const id2 = "bot456";