Skip to content
Merged
3 changes: 3 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -944,6 +946,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",
Expand Down
12 changes: 11 additions & 1 deletion src/client/ClientGameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
28 changes: 28 additions & 0 deletions src/client/UserSettingModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -742,6 +761,15 @@ export class UserSettingModal extends BaseModal {
@change=${this.toggleDarkMode}
></setting-toggle>

<!-- 🎨 Colorblind Mode -->
<setting-toggle
label="${translateText("user_setting.colorblind_label")}"
description="${translateText("user_setting.colorblind_desc")}"
id="colorblind-toggle"
.checked=${this.colorblindMode()}
@change=${this.toggleColorblindMode}
></setting-toggle>

<!-- 😊 Emojis -->
<setting-toggle
label="${translateText("user_setting.emojis_label")}"
Expand Down
13 changes: 13 additions & 0 deletions src/client/WebGLFrameBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ export class WebGLFrameBuilder {
this.skinsInitialized = false;
}

/**
* Re-write every player's palette entry from their current (possibly re-themed)
* colors and re-upload just the palette texture. Used after a mid-game theme
* change (e.g. toggling colorblind mode) so existing territories re-color
* without re-syncing players, skins, or spawns.
*/
refreshPalette(gameView: GameView): void {
for (const p of gameView.players()) {
this.writePaletteEntry(p.smallID(), p.territoryColor(), p.borderColor());
}
this.view.updatePalette(this.palette);
}

update(gameView: GameView): void {
this.syncPlayers(gameView);
this.syncPlayerSpawns(gameView);
Expand Down
50 changes: 50 additions & 0 deletions src/client/hud/layers/GraphicsSettingsModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,18 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
this.requestUpdate();
}

/** Merge a patch into the accessibility graphics overrides and persist it. */
private patchAccessibility(
patch: Partial<GraphicsOverrides["accessibility"]>,
) {
const current = this.userSettings.graphicsOverrides();
this.userSettings.setGraphicsOverrides({
...current,
accessibility: { ...current.accessibility, ...patch },
});
this.requestUpdate();
}
Comment on lines +291 to +301

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make sure it takes effect immediately or people will think the setting is broken. Or we can also remove it from this modal for now. in the future we can add it back and have it take effect immediately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I had that commit staged to make it take effect immediately, but had forgotten to push it. It's part of the PR now.


private currentSpecialEffects(): boolean {
return (
this.userSettings.graphicsOverrides().passEnabled?.fx ??
Expand All @@ -299,6 +311,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 });
Expand Down Expand Up @@ -339,6 +363,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`
<div
Expand Down Expand Up @@ -675,6 +700,31 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
</div>
</button>

<div
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider mt-2"
>
${translateText("graphics_setting.section_accessibility")}
</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.onToggleColorblind}
>
<div class="flex-1">
<div class="font-medium">
${translateText("user_setting.colorblind_label")}
</div>
<div class="text-sm text-slate-400">
${translateText("user_setting.colorblind_desc")}
</div>
</div>
<div class="text-sm text-slate-400">
${colorblind
? translateText("user_setting.on")
: translateText("user_setting.off")}
</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"
Expand Down
5 changes: 5 additions & 0 deletions src/client/render/gl/GraphicsOverrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export const GraphicsOverridesSchema = z
fx: z.boolean(),
})
.partial(),
accessibility: z
.object({
colorblind: z.boolean(),
})
.partial(),
})
.partial();

Expand Down
33 changes: 33 additions & 0 deletions src/client/render/gl/RenderOverrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import type { RenderSettings } from "./RenderSettings";

const DARK_AMBIENT = 0.35;

/**
* Apply the user's graphics overrides onto a RenderSettings in place: name
* scaling, classic/dark structure and name styling, and the colorblind-safe
* affiliation/tint palette.
*/
export function applyGraphicsOverrides(
settings: RenderSettings,
overrides: GraphicsOverrides,
Expand Down Expand Up @@ -64,8 +69,36 @@ export function applyGraphicsOverrides(
settings.name.outlineG = channel;
settings.name.outlineB = channel;
}
if (overrides.accessibility?.colorblind === true) {
// 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.
settings.affiliation.selfR = 0;
settings.affiliation.selfG = 0.447;
settings.affiliation.selfB = 0.698;
settings.affiliation.allyR = 0.337;
settings.affiliation.allyG = 0.706;
settings.affiliation.allyB = 0.914;
settings.affiliation.enemyR = 0.835;
settings.affiliation.enemyG = 0.369;
settings.affiliation.enemyB = 0;
// Normal-view relationship border tints: friendly blue, enemy orange,
// applied strongly so the cue doesn't rely on subtle hue.
settings.mapOverlay.friendlyTintR = 0;
settings.mapOverlay.friendlyTintG = 0.447;
settings.mapOverlay.friendlyTintB = 0.698;
settings.mapOverlay.embargoTintR = 0.835;
settings.mapOverlay.embargoTintG = 0.369;
settings.mapOverlay.embargoTintB = 0;
// Strong ratio so the friend/foe tint dominates the darkened territory
// border — neutral keeps its (darkened) fill hue, ally reads blue, enemy
// reads orange.
settings.mapOverlay.friendlyTintRatio = 0.85;
settings.mapOverlay.embargoTintRatio = 0.85;
}
}

/** Apply dark-mode lighting (ambient + enabled) onto settings when active. */
export function applyDarkModeOverride(
settings: RenderSettings,
isDark: boolean,
Expand Down
Loading
Loading