mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 08:20:25 +00:00
Replace dark mode with player-adjustable lighting (#4280)
## What Removes the binary **dark mode** feature and replaces it with a player-adjustable **Lighting** section in graphics settings. ### In-game settings - Removed the Dark Mode toggle from both `SettingsModal` and `UserSettingModal`, and `darkMode()`/`toggleDarkMode()`/`DARK_MODE_KEY` from `UserSettings`. ### New Lighting section (Graphics Settings) - **Ambient light** slider (1–3): mapped to the renderer's ambient as `ambient = 1 / level`. **1.0 = no effect (unchanged look), 3.0 = darkest with the strongest structure glow.** - **Light falloff** slider (1–3): writes straight to `lighting.falloffPower`. - Lighting auto-enables only when ambient < 1, so the default (slider at 1) has zero GPU cost — off by default. ### Removed dark-mode overrides - Deleted `applyDarkModeOverride()` + `DARK_AMBIENT` and their wiring in `ClientGameRunner`, `gl/index.ts`, and the `DARK_MODE_KEY` listener. - Removed the `.dark` HUD-class toggle in `Main.ts` and the `userSettings.darkMode()` read in `PlayerIcons`. ### Train glow - `UT_TRAIN` light reduced (intensity `2.0 → 0.5`, radius `8 → 6`) so structures dominate the glow. ## Notes - Removing the dark-mode setting also retires the HUD's Tailwind dark theme (same setting). The dormant `dark:` CSS variants and unused white-icon assets are left in place (out of scope). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -537,6 +537,10 @@
|
|||||||
"hover_glow_width_label": "Hover glow size",
|
"hover_glow_width_label": "Hover glow size",
|
||||||
"icon_size_desc": "How large structure icons are drawn on the map",
|
"icon_size_desc": "How large structure icons are drawn on the map",
|
||||||
"icon_size_label": "Structure icon size",
|
"icon_size_label": "Structure icon size",
|
||||||
|
"lighting_ambient_desc": "Darkens the map and adds a glow around structures (0 = off)",
|
||||||
|
"lighting_ambient_label": "Ambient light",
|
||||||
|
"lighting_unit_glow_desc": "How far the glow spreads around units and structures",
|
||||||
|
"lighting_unit_glow_label": "Unit glow",
|
||||||
"name_cull_desc": "Hide names smaller than this size",
|
"name_cull_desc": "Hide names smaller than this size",
|
||||||
"name_cull_label": "Minimum name size",
|
"name_cull_label": "Minimum name size",
|
||||||
"name_scale_label": "Name Scale",
|
"name_scale_label": "Name Scale",
|
||||||
@@ -550,6 +554,7 @@
|
|||||||
"reset_label": "Reset to defaults",
|
"reset_label": "Reset to defaults",
|
||||||
"section_accessibility": "Accessibility",
|
"section_accessibility": "Accessibility",
|
||||||
"section_effects": "Effects",
|
"section_effects": "Effects",
|
||||||
|
"section_lighting": "Lighting",
|
||||||
"section_map": "Map",
|
"section_map": "Map",
|
||||||
"section_name_labels": "Name Labels",
|
"section_name_labels": "Name Labels",
|
||||||
"section_structure_icons": "Structure Icons",
|
"section_structure_icons": "Structure Icons",
|
||||||
@@ -1345,8 +1350,6 @@
|
|||||||
"coordinate_grid_label": "Coordinate Grid",
|
"coordinate_grid_label": "Coordinate Grid",
|
||||||
"cursor_cost_label_desc": "Show a cost pill under the build cursor icon",
|
"cursor_cost_label_desc": "Show a cost pill under the build cursor icon",
|
||||||
"cursor_cost_label_label": "Cursor Build Cost",
|
"cursor_cost_label_label": "Cursor Build Cost",
|
||||||
"dark_mode_desc": "Toggle the site’s appearance between light and dark themes",
|
|
||||||
"dark_mode_label": "Dark Mode",
|
|
||||||
"development_only": "Development Only",
|
"development_only": "Development Only",
|
||||||
"easter_bug_count_desc": "How many bugs you're okay with (0–1000, emotionally)",
|
"easter_bug_count_desc": "How many bugs you're okay with (0–1000, emotionally)",
|
||||||
"easter_bug_count_label": "Bug Count",
|
"easter_bug_count_label": "Bug Count",
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
} from "../core/game/GameUpdates";
|
} from "../core/game/GameUpdates";
|
||||||
import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader";
|
import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader";
|
||||||
import {
|
import {
|
||||||
DARK_MODE_KEY,
|
|
||||||
GRAPHICS_KEY,
|
GRAPHICS_KEY,
|
||||||
USER_SETTINGS_CHANGED_EVENT,
|
USER_SETTINGS_CHANGED_EVENT,
|
||||||
UserSettings,
|
UserSettings,
|
||||||
@@ -68,7 +67,6 @@ import { createCanvas } from "./Utils";
|
|||||||
import { WebGLFrameBuilder } from "./WebGLFrameBuilder";
|
import { WebGLFrameBuilder } from "./WebGLFrameBuilder";
|
||||||
import { createRenderer, GameRenderer } from "./hud/GameRenderer";
|
import { createRenderer, GameRenderer } from "./hud/GameRenderer";
|
||||||
import {
|
import {
|
||||||
applyDarkModeOverride,
|
|
||||||
applyGraphicsOverrides,
|
applyGraphicsOverrides,
|
||||||
createRenderSettings,
|
createRenderSettings,
|
||||||
deepAssign,
|
deepAssign,
|
||||||
@@ -495,7 +493,6 @@ async function createClientGame(
|
|||||||
const resolveRenderSettings = (): RenderSettings => {
|
const resolveRenderSettings = (): RenderSettings => {
|
||||||
const settings = createRenderSettings();
|
const settings = createRenderSettings();
|
||||||
applyGraphicsOverrides(settings, userSettings.graphicsOverrides());
|
applyGraphicsOverrides(settings, userSettings.graphicsOverrides());
|
||||||
applyDarkModeOverride(settings, userSettings.darkMode());
|
|
||||||
return settings;
|
return settings;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -538,11 +535,6 @@ async function createClientGame(
|
|||||||
onGraphicsChanged,
|
onGraphicsChanged,
|
||||||
{ signal: graphicsListenerAbort.signal },
|
{ signal: graphicsListenerAbort.signal },
|
||||||
);
|
);
|
||||||
globalThis.addEventListener(
|
|
||||||
`${USER_SETTINGS_CHANGED_EVENT}:${DARK_MODE_KEY}`,
|
|
||||||
regenerateRenderSettings,
|
|
||||||
{ signal: graphicsListenerAbort.signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Loaded on demand so lil-gui and the debug GUI stay out of the main bundle.
|
// Loaded on demand so lil-gui and the debug GUI stay out of the main bundle.
|
||||||
let debugGui: { open(): void; destroy(): void } | null = null;
|
let debugGui: { open(): void; destroy(): void } | null = null;
|
||||||
|
|||||||
+1
-28
@@ -12,11 +12,7 @@ import {
|
|||||||
} from "../core/Schemas";
|
} from "../core/Schemas";
|
||||||
import { GameEnv } from "../core/configuration/Config";
|
import { GameEnv } from "../core/configuration/Config";
|
||||||
import { GameType } from "../core/game/Game";
|
import { GameType } from "../core/game/Game";
|
||||||
import {
|
import { UserSettings } from "../core/game/UserSettings";
|
||||||
DARK_MODE_KEY,
|
|
||||||
USER_SETTINGS_CHANGED_EVENT,
|
|
||||||
UserSettings,
|
|
||||||
} from "../core/game/UserSettings";
|
|
||||||
import "./AccountModal";
|
import "./AccountModal";
|
||||||
import { getUserMe, invalidateUserMe } from "./Api";
|
import { getUserMe, invalidateUserMe } from "./Api";
|
||||||
import { userAuth } from "./Auth";
|
import { userAuth } from "./Auth";
|
||||||
@@ -226,11 +222,6 @@ declare global {
|
|||||||
"leave-lobby": CustomEvent;
|
"leave-lobby": CustomEvent;
|
||||||
"update-game-config": CustomEvent;
|
"update-game-config": CustomEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixes the globalThis.addEventListener errors
|
|
||||||
interface WindowEventMap {
|
|
||||||
"event:user-settings-changed:settings.darkMode": CustomEvent<string>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JoinLobbyEvent {
|
export interface JoinLobbyEvent {
|
||||||
@@ -547,24 +538,6 @@ class Client {
|
|||||||
this.joinModal.eventBus = this.eventBus;
|
this.joinModal.eventBus = this.eventBus;
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyDarkMode = (isDark: boolean) => {
|
|
||||||
if (isDark) {
|
|
||||||
document.documentElement.classList.add("dark");
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove("dark");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
applyDarkMode(this.userSettings.darkMode());
|
|
||||||
|
|
||||||
globalThis.addEventListener(
|
|
||||||
`${USER_SETTINGS_CHANGED_EVENT}:${DARK_MODE_KEY}`,
|
|
||||||
(e: CustomEvent<string>) => {
|
|
||||||
const isDark = e.detail === "true";
|
|
||||||
applyDarkMode(isDark);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Attempt to join lobby
|
// Attempt to join lobby
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", () => this.handleUrl());
|
document.addEventListener("DOMContentLoaded", () => this.handleUrl());
|
||||||
|
|||||||
@@ -200,12 +200,6 @@ export class UserSettingModal extends BaseModal {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDarkMode() {
|
|
||||||
this.userSettings.toggleDarkMode();
|
|
||||||
|
|
||||||
console.log("🌙 Dark Mode:", this.userSettings.darkMode() ? "ON" : "OFF");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Whether colorblind mode is currently enabled in the graphics overrides. */
|
/** Whether colorblind mode is currently enabled in the graphics overrides. */
|
||||||
private colorblindMode(): boolean {
|
private colorblindMode(): boolean {
|
||||||
return (
|
return (
|
||||||
@@ -752,15 +746,6 @@ export class UserSettingModal extends BaseModal {
|
|||||||
|
|
||||||
private renderBasicSettings() {
|
private renderBasicSettings() {
|
||||||
return html`
|
return html`
|
||||||
<!-- 🌙 Dark Mode -->
|
|
||||||
<setting-toggle
|
|
||||||
label="${translateText("user_setting.dark_mode_label")}"
|
|
||||||
description="${translateText("user_setting.dark_mode_desc")}"
|
|
||||||
id="dark-mode-toggle"
|
|
||||||
.checked=${this.userSettings.darkMode()}
|
|
||||||
@change=${this.toggleDarkMode}
|
|
||||||
></setting-toggle>
|
|
||||||
|
|
||||||
<!-- 🎨 Colorblind Mode -->
|
<!-- 🎨 Colorblind Mode -->
|
||||||
<setting-toggle
|
<setting-toggle
|
||||||
label="${translateText("user_setting.colorblind_label")}"
|
label="${translateText("user_setting.colorblind_label")}"
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export function getPlayerIcons(
|
|||||||
|
|
||||||
const myPlayer = game.myPlayer();
|
const myPlayer = game.myPlayer();
|
||||||
const userSettings = game.config().userSettings();
|
const userSettings = game.config().userSettings();
|
||||||
const isDarkMode = darkMode ?? userSettings?.darkMode() ?? false;
|
const isDarkMode = darkMode ?? false;
|
||||||
const emojisEnabled = userSettings?.emojis() ?? false;
|
const emojisEnabled = userSettings?.emojis() ?? false;
|
||||||
const alliancesOff = alliancesDisabled ?? game.config().disableAlliances();
|
const alliancesOff = alliancesDisabled ?? game.config().disableAlliances();
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,49 @@ const RAIL_THICKNESS_MIN = 0.5;
|
|||||||
const RAIL_THICKNESS_MAX = 3;
|
const RAIL_THICKNESS_MAX = 3;
|
||||||
const RAIL_THICKNESS_STEP = 0.1;
|
const RAIL_THICKNESS_STEP = 0.1;
|
||||||
|
|
||||||
|
// "Ambient light" level shown to the player: 0 = no darkening (lighting off),
|
||||||
|
// 10 = darkest with the strongest glow. Mapped linearly onto the renderer's
|
||||||
|
// ambient value (1 = identity, AMBIENT_MIN = darkest).
|
||||||
|
const AMBIENT_LEVEL_MIN = 0;
|
||||||
|
const AMBIENT_LEVEL_MAX = 10;
|
||||||
|
const AMBIENT_LEVEL_STEP = 1;
|
||||||
|
const AMBIENT_MIN = 0.2;
|
||||||
|
|
||||||
|
function ambientSliderToValue(slider: number): number {
|
||||||
|
return 1 - (slider / AMBIENT_LEVEL_MAX) * (1 - AMBIENT_MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ambientValueToSlider(ambient: number): number {
|
||||||
|
const slider = ((1 - ambient) / (1 - AMBIENT_MIN)) * AMBIENT_LEVEL_MAX;
|
||||||
|
return Math.round(
|
||||||
|
Math.min(AMBIENT_LEVEL_MAX, Math.max(AMBIENT_LEVEL_MIN, slider)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Unit glow" level shown to the player: higher = more glow. It's the inverse
|
||||||
|
// of the renderer's falloffPower (lower power spreads the glow wider), mapped
|
||||||
|
// so 0 = tightest (FALLOFF_AT_MIN_GLOW) and 10 = widest (FALLOFF_AT_MAX_GLOW).
|
||||||
|
const UNIT_GLOW_MIN = 0;
|
||||||
|
const UNIT_GLOW_MAX = 10;
|
||||||
|
const UNIT_GLOW_STEP = 1;
|
||||||
|
const FALLOFF_AT_MIN_GLOW = 3;
|
||||||
|
const FALLOFF_AT_MAX_GLOW = 1;
|
||||||
|
|
||||||
|
function unitGlowSliderToFalloff(slider: number): number {
|
||||||
|
return (
|
||||||
|
FALLOFF_AT_MIN_GLOW -
|
||||||
|
(slider / UNIT_GLOW_MAX) * (FALLOFF_AT_MIN_GLOW - FALLOFF_AT_MAX_GLOW)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function falloffToUnitGlowSlider(falloff: number): number {
|
||||||
|
const slider =
|
||||||
|
((FALLOFF_AT_MIN_GLOW - falloff) /
|
||||||
|
(FALLOFF_AT_MIN_GLOW - FALLOFF_AT_MAX_GLOW)) *
|
||||||
|
UNIT_GLOW_MAX;
|
||||||
|
return Math.round(Math.min(UNIT_GLOW_MAX, Math.max(UNIT_GLOW_MIN, slider)));
|
||||||
|
}
|
||||||
|
|
||||||
const HEX_COLOR_RE = /^#?([0-9a-fA-F]{6})$/;
|
const HEX_COLOR_RE = /^#?([0-9a-fA-F]{6})$/;
|
||||||
|
|
||||||
export class ShowGraphicsSettingsModalEvent {
|
export class ShowGraphicsSettingsModalEvent {
|
||||||
@@ -359,6 +402,39 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
|||||||
this.patchTerrain({ oceanColor: `#${match[1].toLowerCase()}` });
|
this.patchTerrain({ oceanColor: `#${match[1].toLowerCase()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private patchLighting(patch: Partial<GraphicsOverrides["lighting"]>) {
|
||||||
|
const current = this.userSettings.graphicsOverrides();
|
||||||
|
this.userSettings.setGraphicsOverrides({
|
||||||
|
...current,
|
||||||
|
lighting: { ...current.lighting, ...patch },
|
||||||
|
});
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private currentAmbientLevel(): number {
|
||||||
|
const ambient =
|
||||||
|
this.userSettings.graphicsOverrides().lighting?.ambient ??
|
||||||
|
renderDefaults.lighting.ambient;
|
||||||
|
return ambientValueToSlider(ambient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAmbientLevelChange(event: Event) {
|
||||||
|
const level = parseFloat((event.target as HTMLInputElement).value);
|
||||||
|
this.patchLighting({ ambient: ambientSliderToValue(level) });
|
||||||
|
}
|
||||||
|
|
||||||
|
private currentUnitGlow(): number {
|
||||||
|
const falloff =
|
||||||
|
this.userSettings.graphicsOverrides().lighting?.falloffPower ??
|
||||||
|
renderDefaults.lighting.falloffPower;
|
||||||
|
return falloffToUnitGlowSlider(falloff);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onUnitGlowChange(event: Event) {
|
||||||
|
const level = parseFloat((event.target as HTMLInputElement).value);
|
||||||
|
this.patchLighting({ falloffPower: unitGlowSliderToFalloff(level) });
|
||||||
|
}
|
||||||
|
|
||||||
private currentClassicIcons(): boolean {
|
private currentClassicIcons(): boolean {
|
||||||
return (
|
return (
|
||||||
this.userSettings.graphicsOverrides().structure?.classicIcons ?? true
|
this.userSettings.graphicsOverrides().structure?.classicIcons ?? true
|
||||||
@@ -485,6 +561,8 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
|||||||
const railDrawDistance = RAIL_ZOOM_MAX - this.currentRailMinZoom();
|
const railDrawDistance = RAIL_ZOOM_MAX - this.currentRailMinZoom();
|
||||||
const railThickness = this.currentRailThickness();
|
const railThickness = this.currentRailThickness();
|
||||||
const oceanColor = this.currentOceanColor();
|
const oceanColor = this.currentOceanColor();
|
||||||
|
const ambientLevel = this.currentAmbientLevel();
|
||||||
|
const unitGlow = this.currentUnitGlow();
|
||||||
const colorblind = this.currentColorblind();
|
const colorblind = this.currentColorblind();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -521,6 +599,62 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
|||||||
<div class="p-4 flex flex-col gap-3">
|
<div class="p-4 flex flex-col gap-3">
|
||||||
<div
|
<div
|
||||||
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider"
|
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
${translateText("graphics_setting.section_lighting")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="font-medium">
|
||||||
|
${translateText("graphics_setting.lighting_ambient_label")}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-slate-400">
|
||||||
|
${translateText("graphics_setting.lighting_ambient_desc")}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min=${AMBIENT_LEVEL_MIN}
|
||||||
|
max=${AMBIENT_LEVEL_MAX}
|
||||||
|
step=${AMBIENT_LEVEL_STEP}
|
||||||
|
.value=${String(ambientLevel)}
|
||||||
|
@input=${this.onAmbientLevelChange}
|
||||||
|
class="w-full border border-slate-500 rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-slate-400 w-12 text-right">
|
||||||
|
${ambientLevel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="font-medium">
|
||||||
|
${translateText("graphics_setting.lighting_unit_glow_label")}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-slate-400">
|
||||||
|
${translateText("graphics_setting.lighting_unit_glow_desc")}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min=${UNIT_GLOW_MIN}
|
||||||
|
max=${UNIT_GLOW_MAX}
|
||||||
|
step=${UNIT_GLOW_STEP}
|
||||||
|
.value=${String(unitGlow)}
|
||||||
|
@input=${this.onUnitGlowChange}
|
||||||
|
class="w-full border border-slate-500 rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-slate-400 w-12 text-right">
|
||||||
|
${unitGlow}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider mt-2"
|
||||||
>
|
>
|
||||||
${translateText("graphics_setting.section_name_labels")}
|
${translateText("graphics_setting.section_name_labels")}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { UserSettings } from "../../../core/game/UserSettings";
|
|||||||
import { Controller } from "../../Controller";
|
import { Controller } from "../../Controller";
|
||||||
import {
|
import {
|
||||||
AlternateViewEvent,
|
AlternateViewEvent,
|
||||||
RefreshGraphicsEvent,
|
|
||||||
ToggleRenderDebugGuiEvent,
|
ToggleRenderDebugGuiEvent,
|
||||||
} from "../../InputHandler";
|
} from "../../InputHandler";
|
||||||
import { translateText } from "../../Utils";
|
import { translateText } from "../../Utils";
|
||||||
@@ -18,7 +17,6 @@ import {
|
|||||||
} from "../../sound/Sounds";
|
} from "../../sound/Sounds";
|
||||||
import { ShowGraphicsSettingsModalEvent } from "./GraphicsSettingsModal";
|
import { ShowGraphicsSettingsModalEvent } from "./GraphicsSettingsModal";
|
||||||
const cursorPriceIcon = assetUrl("images/CursorPriceIconWhite.svg");
|
const cursorPriceIcon = assetUrl("images/CursorPriceIconWhite.svg");
|
||||||
const darkModeIcon = assetUrl("images/DarkModeIconWhite.svg");
|
|
||||||
const emojiIcon = assetUrl("images/EmojiIconWhite.svg");
|
const emojiIcon = assetUrl("images/EmojiIconWhite.svg");
|
||||||
const exitIcon = assetUrl("images/ExitIconWhite.svg");
|
const exitIcon = assetUrl("images/ExitIconWhite.svg");
|
||||||
const mouseIcon = assetUrl("images/MouseIconWhite.svg");
|
const mouseIcon = assetUrl("images/MouseIconWhite.svg");
|
||||||
@@ -141,12 +139,6 @@ export class SettingsModal extends LitElement implements Controller {
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onToggleDarkModeButtonClick() {
|
|
||||||
this.userSettings.toggleDarkMode();
|
|
||||||
this.eventBus.emit(new RefreshGraphicsEvent());
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private onToggleRandomNameModeButtonClick() {
|
private onToggleRandomNameModeButtonClick() {
|
||||||
this.userSettings.toggleRandomName();
|
this.userSettings.toggleRandomName();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@@ -353,31 +345,6 @@ export class SettingsModal extends LitElement implements Controller {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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.onToggleDarkModeButtonClick}"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src=${darkModeIcon}
|
|
||||||
alt="darkModeIcon"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
/>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="font-medium">
|
|
||||||
${translateText("user_setting.dark_mode_label")}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-slate-400">
|
|
||||||
${translateText("user_setting.dark_mode_desc")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-slate-400">
|
|
||||||
${this.userSettings.darkMode()
|
|
||||||
? translateText("user_setting.on")
|
|
||||||
: translateText("user_setting.off")}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||||
@click="${this.onToggleAlertFrameButtonClick}"
|
@click="${this.onToggleAlertFrameButtonClick}"
|
||||||
|
|||||||
@@ -51,6 +51,15 @@ export const GraphicsOverridesSchema = z
|
|||||||
oceanColor: z.string(),
|
oceanColor: z.string(),
|
||||||
})
|
})
|
||||||
.partial(),
|
.partial(),
|
||||||
|
lighting: z
|
||||||
|
.object({
|
||||||
|
// Scene brightness multiplier in the day/night composite. <1 darkens
|
||||||
|
// the map and reveals the glow around structures/units; 1 is identity.
|
||||||
|
ambient: z.number(),
|
||||||
|
// Exponent controlling how sharply a light fades with distance.
|
||||||
|
falloffPower: z.number(),
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { GraphicsOverrides } from "./GraphicsOverrides";
|
import type { GraphicsOverrides } from "./GraphicsOverrides";
|
||||||
import { createThemeSettings, type RenderSettings } from "./RenderSettings";
|
import { createThemeSettings, type RenderSettings } from "./RenderSettings";
|
||||||
|
|
||||||
const DARK_AMBIENT = 0.35;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the user's graphics overrides onto a RenderSettings in place: name
|
* Apply the user's graphics overrides onto a RenderSettings in place: name
|
||||||
* scaling, classic/dark structure and name styling, and the colorblind-safe
|
* scaling, classic/dark structure and name styling, and the colorblind-safe
|
||||||
@@ -78,6 +76,16 @@ export function applyGraphicsOverrides(
|
|||||||
if (overrides.terrain?.oceanColor !== undefined) {
|
if (overrides.terrain?.oceanColor !== undefined) {
|
||||||
settings.terrain.oceanColor = overrides.terrain.oceanColor;
|
settings.terrain.oceanColor = overrides.terrain.oceanColor;
|
||||||
}
|
}
|
||||||
|
if (overrides.lighting?.ambient !== undefined) {
|
||||||
|
settings.lighting.ambient = overrides.lighting.ambient;
|
||||||
|
// The composite only darkens the scene (and reveals the structure/unit
|
||||||
|
// glow) when ambient < 1; at ambient === 1 it's a visual identity, so
|
||||||
|
// don't pay the scene-capture cost of enabling the lighting pass.
|
||||||
|
settings.lighting.enabled = overrides.lighting.ambient < 1;
|
||||||
|
}
|
||||||
|
if (overrides.lighting?.falloffPower !== undefined) {
|
||||||
|
settings.lighting.falloffPower = overrides.lighting.falloffPower;
|
||||||
|
}
|
||||||
if (overrides.name?.darkNames !== undefined) {
|
if (overrides.name?.darkNames !== undefined) {
|
||||||
const dark = overrides.name.darkNames;
|
const dark = overrides.name.darkNames;
|
||||||
// Dark: black fill + player-colored outline. Force outline RGB to black
|
// Dark: black fill + player-colored outline. Force outline RGB to black
|
||||||
@@ -122,13 +130,3 @@ export function applyGraphicsOverrides(
|
|||||||
settings.mapOverlay.embargoTintRatio = 0.85;
|
settings.mapOverlay.embargoTintRatio = 0.85;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply dark-mode lighting (ambient + enabled) onto settings when active. */
|
|
||||||
export function applyDarkModeOverride(
|
|
||||||
settings: RenderSettings,
|
|
||||||
isDark: boolean,
|
|
||||||
): void {
|
|
||||||
if (!isDark) return;
|
|
||||||
settings.lighting.ambient = DARK_AMBIENT;
|
|
||||||
settings.lighting.enabled = true;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ export type { GraphicsOverrides } from "./GraphicsOverrides";
|
|||||||
export { MapRenderer } from "./MapRenderer";
|
export { MapRenderer } from "./MapRenderer";
|
||||||
export { preloadAtlasData } from "./passes/name-pass/AtlasData";
|
export { preloadAtlasData } from "./passes/name-pass/AtlasData";
|
||||||
export type { SpawnCenter } from "./passes/SpawnOverlayPass";
|
export type { SpawnCenter } from "./passes/SpawnOverlayPass";
|
||||||
export {
|
export { applyGraphicsOverrides } from "./RenderOverrides";
|
||||||
applyDarkModeOverride,
|
|
||||||
applyGraphicsOverrides,
|
|
||||||
} from "./RenderOverrides";
|
|
||||||
export { createRenderSettings, dumpSettings } from "./RenderSettings";
|
export { createRenderSettings, dumpSettings } from "./RenderSettings";
|
||||||
export type { RenderSettings } from "./RenderSettings";
|
export type { RenderSettings } from "./RenderSettings";
|
||||||
export { deepAssign, deepDiff } from "./SettingsUtils";
|
export { deepAssign, deepDiff } from "./SettingsUtils";
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ const LIGHT_CONFIGS: Record<string, LightConfig> = {
|
|||||||
[UT_HYDROGEN_BOMB]: { r: 1.0, g: 0.95, b: 0.6, radius: 22, intensity: 1.3 },
|
[UT_HYDROGEN_BOMB]: { r: 1.0, g: 0.95, b: 0.6, radius: 22, intensity: 1.3 },
|
||||||
[UT_MIRV]: { r: 1.0, g: 0.9, b: 0.7, radius: 18, intensity: 1.2 },
|
[UT_MIRV]: { r: 1.0, g: 0.9, b: 0.7, radius: 18, intensity: 1.2 },
|
||||||
[UT_MIRV_WARHEAD]: { r: 1.0, g: 0.6, b: 0.3, radius: 12, intensity: 1.0 },
|
[UT_MIRV_WARHEAD]: { r: 1.0, g: 0.6, b: 0.3, radius: 12, intensity: 1.0 },
|
||||||
[UT_TRAIN]: { r: 1.0, g: 0.85, b: 0.5, radius: 8, intensity: 2.0 },
|
// A train is many UT_TRAIN units (engine + tail + carriages) in a line, and
|
||||||
|
// lights blend additively — keep per-unit intensity low (~a trade ship's
|
||||||
|
// brightness ÷ car count) so the train corridor doesn't blow out.
|
||||||
|
[UT_TRAIN]: { r: 1.0, g: 0.85, b: 0.5, radius: 6, intensity: 0.5 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const FLOATS_PER_LIGHT = 6;
|
const FLOATS_PER_LIGHT = 6;
|
||||||
|
|||||||
@@ -378,8 +378,8 @@
|
|||||||
"intensity": 1
|
"intensity": 1
|
||||||
},
|
},
|
||||||
"Train": {
|
"Train": {
|
||||||
"radius": 8,
|
"radius": 6,
|
||||||
"intensity": 2
|
"intensity": 0.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ export const USER_SETTINGS_CHANGED_EVENT = "event:user-settings-changed";
|
|||||||
export const PATTERN_KEY = "territoryPattern";
|
export const PATTERN_KEY = "territoryPattern";
|
||||||
export const FLAG_KEY = "flag";
|
export const FLAG_KEY = "flag";
|
||||||
export const COLOR_KEY = "settings.territoryColor";
|
export const COLOR_KEY = "settings.territoryColor";
|
||||||
export const DARK_MODE_KEY = "settings.darkMode";
|
|
||||||
export const PERFORMANCE_OVERLAY_KEY = "settings.performanceOverlay";
|
export const PERFORMANCE_OVERLAY_KEY = "settings.performanceOverlay";
|
||||||
export const KEYBINDS_KEY = "settings.keybinds";
|
export const KEYBINDS_KEY = "settings.keybinds";
|
||||||
export const GRAPHICS_KEY = "settings.graphics";
|
export const GRAPHICS_KEY = "settings.graphics";
|
||||||
@@ -154,10 +153,6 @@ export class UserSettings {
|
|||||||
return this.getBool("settings.lobbyIdVisibility", true);
|
return this.getBool("settings.lobbyIdVisibility", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
darkMode() {
|
|
||||||
return this.getBool(DARK_MODE_KEY, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
leftClickOpensMenu() {
|
leftClickOpensMenu() {
|
||||||
return this.getBool("settings.leftClickOpensMenu", false);
|
return this.getBool("settings.leftClickOpensMenu", false);
|
||||||
}
|
}
|
||||||
@@ -235,10 +230,6 @@ export class UserSettings {
|
|||||||
this.setBool("settings.goToPlayer", !this.goToPlayer());
|
this.setBool("settings.goToPlayer", !this.goToPlayer());
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDarkMode() {
|
|
||||||
this.setBool(DARK_MODE_KEY, !this.darkMode());
|
|
||||||
}
|
|
||||||
|
|
||||||
// For development only. Used for testing patterns, set in the console manually.
|
// For development only. Used for testing patterns, set in the console manually.
|
||||||
getDevOnlyPattern(): PlayerPattern | undefined {
|
getDevOnlyPattern(): PlayerPattern | undefined {
|
||||||
const data = localStorage.getItem("dev-pattern") ?? undefined;
|
const data = localStorage.getItem("dev-pattern") ?? undefined;
|
||||||
|
|||||||
@@ -74,6 +74,19 @@ describe("GraphicsOverridesSchema", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("accepts partial lighting overrides", () => {
|
||||||
|
const cases = [
|
||||||
|
{ lighting: {} },
|
||||||
|
{ lighting: { ambient: 0.5 } },
|
||||||
|
{ lighting: { ambient: 1 } },
|
||||||
|
{ lighting: { falloffPower: 2 } },
|
||||||
|
{ lighting: { ambient: 0.3, falloffPower: 1.5 } },
|
||||||
|
];
|
||||||
|
for (const c of cases) {
|
||||||
|
expect(GraphicsOverridesSchema.safeParse(c).success).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("rejects wrong field types", () => {
|
test("rejects wrong field types", () => {
|
||||||
expect(
|
expect(
|
||||||
GraphicsOverridesSchema.safeParse({ name: { nameScaleFactor: "big" } })
|
GraphicsOverridesSchema.safeParse({ name: { nameScaleFactor: "big" } })
|
||||||
@@ -115,6 +128,16 @@ describe("GraphicsOverridesSchema", () => {
|
|||||||
railroad: { railThickness: "wide" },
|
railroad: { railThickness: "wide" },
|
||||||
}).success,
|
}).success,
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
GraphicsOverridesSchema.safeParse({
|
||||||
|
lighting: { ambient: "dark" },
|
||||||
|
}).success,
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
GraphicsOverridesSchema.safeParse({
|
||||||
|
lighting: { falloffPower: "soft" },
|
||||||
|
}).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -351,6 +374,55 @@ describe("applyGraphicsOverrides", () => {
|
|||||||
expect(z.railThickness).toBe(defaults.railThickness);
|
expect(z.railThickness).toBe(defaults.railThickness);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ambient < 1 sets ambient and enables the lighting pass", () => {
|
||||||
|
const l = gen({ lighting: { ambient: 0.5 } }).lighting;
|
||||||
|
expect(l.ambient).toBe(0.5);
|
||||||
|
expect(l.enabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ambient === 1 sets ambient but leaves lighting disabled (identity)", () => {
|
||||||
|
const l = gen({ lighting: { ambient: 1 } }).lighting;
|
||||||
|
expect(l.ambient).toBe(1);
|
||||||
|
expect(l.enabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ambient absent → lighting stays at render-settings.json defaults", () => {
|
||||||
|
const defaults = createRenderSettings().lighting;
|
||||||
|
expect(gen({}).lighting.ambient).toBe(defaults.ambient);
|
||||||
|
expect(gen({}).lighting.enabled).toBe(defaults.enabled);
|
||||||
|
expect(gen({ lighting: {} }).lighting.enabled).toBe(defaults.enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("applies falloffPower override (including values below default)", () => {
|
||||||
|
expect(gen({ lighting: { falloffPower: 1.4 } }).lighting.falloffPower).toBe(
|
||||||
|
1.4,
|
||||||
|
);
|
||||||
|
expect(gen({ lighting: { falloffPower: 3 } }).lighting.falloffPower).toBe(
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falloffPower override alone does not enable the lighting pass", () => {
|
||||||
|
expect(gen({ lighting: { falloffPower: 1.4 } }).lighting.enabled).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lighting override leaves other lighting fields at defaults", () => {
|
||||||
|
const defaults = createRenderSettings().lighting;
|
||||||
|
const l = gen({ lighting: { ambient: 0.4 } }).lighting;
|
||||||
|
expect(l.falloffPower).toBe(defaults.falloffPower);
|
||||||
|
expect(l.blurZoomDivisor).toBe(defaults.blurZoomDivisor);
|
||||||
|
expect(l.lightRadiusMultiplier).toBe(defaults.lightRadiusMultiplier);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ambient + falloffPower compose together", () => {
|
||||||
|
const l = gen({ lighting: { ambient: 0.3, falloffPower: 1 } }).lighting;
|
||||||
|
expect(l.ambient).toBe(0.3);
|
||||||
|
expect(l.falloffPower).toBe(1);
|
||||||
|
expect(l.enabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("classicIcons + name overrides compose independently", () => {
|
test("classicIcons + name overrides compose independently", () => {
|
||||||
const s = gen({
|
const s = gen({
|
||||||
name: { darkNames: true, nameScaleFactor: 0.9 },
|
name: { darkNames: true, nameScaleFactor: 0.9 },
|
||||||
|
|||||||
Reference in New Issue
Block a user