mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 06:20:44 +00:00
Add Graphics Settings modal with live name-label tuning (#4065)
## Description:
- Add a user-facing **Graphics Settings** modal accessible from the
in-game Settings menu, with live preview as sliders change.
- First two knobs: **Name Scale** and **Minimum Name Size** (the
name-cull threshold).
- Overrides stored as a single JSON blob in `localStorage` under
`settings.graphics`, validated by a Zod schema
(`GraphicsOverridesSchema`). Future graphics knobs just extend the
schema + slider list.
## How it fits together
- `generateRenderSettings(overrides)` (`RenderSettings.ts`) — pure
function: clones `render-settings.json` defaults, layers overrides on
top, returns a fresh `RenderSettings`.
- `UserSettings.graphicsOverrides()` / `setGraphicsOverrides()` —
read/write the blob; falls back to `{}` on a missing/corrupt entry.
- `ClientGameRunner` listens for
`USER_SETTINGS_CHANGED_EVENT:settings.graphics`, regenerates, and
`Object.assign`s each category into the live `view.getSettings()` slice
so passes pick up the new values on the next frame (no renderer
reconstruction).
- Modal reads defaults straight from `render-settings.json` so there's
no duplication.
<img width="599" height="515" alt="Screenshot 2026-05-28 at 11 18 43 AM"
src="https://github.com/user-attachments/assets/263d7d91-10d8-4a66-a069-10015c735d60"
/>
## 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
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
evan
This commit is contained in:
@@ -341,6 +341,7 @@
|
||||
<replay-panel></replay-panel>
|
||||
</div>
|
||||
<settings-modal></settings-modal>
|
||||
<graphics-settings-modal></graphics-settings-modal>
|
||||
<player-panel></player-panel>
|
||||
<spawn-timer></spawn-timer>
|
||||
<immunity-timer></immunity-timer>
|
||||
|
||||
@@ -827,6 +827,8 @@
|
||||
"render_debug_gui": "Render Debug GUI",
|
||||
"render_debug_gui_desc": "Toggle the renderer tuning panel",
|
||||
"development_only": "Development Only",
|
||||
"graphics_settings_label": "Graphics Settings",
|
||||
"graphics_settings_desc": "Adjust how the map looks",
|
||||
"easter_writing_speed_label": "Writing Speed Multiplier",
|
||||
"easter_writing_speed_desc": "Adjust how fast you pretend to code (x1–x100)",
|
||||
"easter_bug_count_label": "Bug Count",
|
||||
@@ -915,6 +917,15 @@
|
||||
"sound_effects_volume": "Sound Effects Volume",
|
||||
"keybind_conflict_error": "The key {key} is already bound to another action."
|
||||
},
|
||||
"graphics_setting": {
|
||||
"title": "Graphics Settings",
|
||||
"section_name_labels": "Name Labels",
|
||||
"name_scale_label": "Name Scale",
|
||||
"name_cull_label": "Minimum name size",
|
||||
"name_cull_desc": "Hide names smaller than this size",
|
||||
"reset_label": "Reset to defaults",
|
||||
"reset_desc": "Clear all graphics overrides"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Quick Chat",
|
||||
"to": "Sent {user}: {msg}",
|
||||
|
||||
@@ -31,6 +31,7 @@ import { GameView, PlayerView } from "../core/game/GameView";
|
||||
import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader";
|
||||
import {
|
||||
DARK_MODE_KEY,
|
||||
GRAPHICS_KEY,
|
||||
USER_SETTINGS_CHANGED_EVENT,
|
||||
UserSettings,
|
||||
} from "../core/game/UserSettings";
|
||||
@@ -67,7 +68,11 @@ import {
|
||||
import { createCanvas } from "./Utils";
|
||||
import { WebGLFrameBuilder } from "./WebGLFrameBuilder";
|
||||
import { createRenderer, GameRenderer } from "./hud/GameRenderer";
|
||||
import { createDebugGui, GameView as WebGLGameView } from "./render/gl";
|
||||
import {
|
||||
createDebugGui,
|
||||
generateRenderSettings,
|
||||
GameView as WebGLGameView,
|
||||
} from "./render/gl";
|
||||
import { ALL_UNIT_TYPES, UnitState } from "./render/types";
|
||||
import { SoundManager } from "./sound/SoundManager";
|
||||
|
||||
@@ -479,6 +484,21 @@ async function createClientGame(
|
||||
(e) => view.setShowPatterns((e as CustomEvent<string>).detail === "true"),
|
||||
);
|
||||
|
||||
const graphicsListenerAbort = new AbortController();
|
||||
const applyGraphicsOverrides = (): void => {
|
||||
const generated = generateRenderSettings(
|
||||
userSettings.graphicsOverrides(),
|
||||
);
|
||||
const live = view.getSettings();
|
||||
Object.assign(live.name, generated.name);
|
||||
};
|
||||
applyGraphicsOverrides();
|
||||
globalThis.addEventListener(
|
||||
`${USER_SETTINGS_CHANGED_EVENT}:${GRAPHICS_KEY}`,
|
||||
applyGraphicsOverrides,
|
||||
{ signal: graphicsListenerAbort.signal },
|
||||
);
|
||||
|
||||
let debugGui: ReturnType<typeof createDebugGui> | null = null;
|
||||
eventBus.on(ToggleRenderDebugGuiEvent, () => {
|
||||
if (debugGui === null) {
|
||||
@@ -524,6 +544,7 @@ async function createClientGame(
|
||||
soundManager,
|
||||
userSettings,
|
||||
webglBuilder,
|
||||
graphicsListenerAbort,
|
||||
);
|
||||
} catch (err) {
|
||||
soundManager.dispose();
|
||||
@@ -557,6 +578,7 @@ export class ClientGameRunner {
|
||||
private soundManager: SoundManager,
|
||||
private userSettings: UserSettings,
|
||||
private webglBuilder: WebGLFrameBuilder | null = null,
|
||||
private graphicsListenerAbort: AbortController | null = null,
|
||||
) {
|
||||
this.lastMessageTime = Date.now();
|
||||
}
|
||||
@@ -813,6 +835,7 @@ export class ClientGameRunner {
|
||||
|
||||
public stop() {
|
||||
this.soundManager.dispose();
|
||||
this.graphicsListenerAbort?.abort();
|
||||
if (!this.isActive) return;
|
||||
|
||||
this.isActive = false;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { EmojiTable } from "./layers/EmojiTable";
|
||||
import { EventsDisplay } from "./layers/EventsDisplay";
|
||||
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
|
||||
import { GameRightSidebar } from "./layers/GameRightSidebar";
|
||||
import { GraphicsSettingsModal } from "./layers/GraphicsSettingsModal";
|
||||
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
|
||||
import { ImmunityTimer } from "./layers/ImmunityTimer";
|
||||
import { InGamePromo } from "./layers/InGamePromo";
|
||||
@@ -190,6 +191,15 @@ export function createRenderer(
|
||||
settingsModal.userSettings = userSettings;
|
||||
settingsModal.eventBus = eventBus;
|
||||
|
||||
const graphicsSettingsModal = document.querySelector(
|
||||
"graphics-settings-modal",
|
||||
) as GraphicsSettingsModal;
|
||||
if (!(graphicsSettingsModal instanceof GraphicsSettingsModal)) {
|
||||
console.error("graphics settings modal not found");
|
||||
}
|
||||
graphicsSettingsModal.userSettings = userSettings;
|
||||
graphicsSettingsModal.eventBus = eventBus;
|
||||
|
||||
const unitDisplay = document.querySelector("unit-display") as UnitDisplay;
|
||||
if (!(unitDisplay instanceof UnitDisplay)) {
|
||||
console.error("unit display not found");
|
||||
@@ -309,6 +319,7 @@ export function createRenderer(
|
||||
winModal,
|
||||
replayPanel,
|
||||
settingsModal,
|
||||
graphicsSettingsModal,
|
||||
teamStats,
|
||||
playerPanel,
|
||||
headsUpMessage,
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { crazyGamesSDK } from "src/client/CrazyGamesSDK";
|
||||
import { PauseGameIntentEvent } from "src/client/Transport";
|
||||
import { assetUrl } from "../../../core/AssetUrls";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { Controller } from "../../Controller";
|
||||
import { translateText } from "../../Utils";
|
||||
import type { GraphicsOverrides } from "../../render/gl";
|
||||
import renderDefaults from "../../render/gl/render-settings.json";
|
||||
|
||||
const settingsIcon = assetUrl("images/SettingIconWhite.svg");
|
||||
|
||||
const NAME_SCALE_MIN = 0.2;
|
||||
const NAME_SCALE_MAX = 1.5;
|
||||
const NAME_SCALE_STEP = 0.05;
|
||||
|
||||
const NAME_CULL_MIN = 0;
|
||||
const NAME_CULL_MAX = 0.05;
|
||||
const NAME_CULL_STEP = 0.001;
|
||||
|
||||
export class ShowGraphicsSettingsModalEvent {
|
||||
constructor(
|
||||
public readonly isVisible: boolean = true,
|
||||
public readonly shouldPause: boolean = false,
|
||||
public readonly isPaused: boolean = false,
|
||||
) {}
|
||||
}
|
||||
|
||||
@customElement("graphics-settings-modal")
|
||||
export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
public eventBus: EventBus;
|
||||
public userSettings: UserSettings;
|
||||
|
||||
@state()
|
||||
private isVisible: boolean = false;
|
||||
|
||||
@query(".modal-overlay")
|
||||
private modalOverlay!: HTMLElement;
|
||||
|
||||
@property({ type: Boolean })
|
||||
shouldPause = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
wasPausedWhenOpened = false;
|
||||
|
||||
init() {
|
||||
this.eventBus.on(ShowGraphicsSettingsModalEvent, (event) => {
|
||||
this.isVisible = event.isVisible;
|
||||
this.shouldPause = event.shouldPause;
|
||||
this.wasPausedWhenOpened = event.isPaused;
|
||||
this.pauseGame(true);
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
private pauseGame(pause: boolean) {
|
||||
if (this.shouldPause && !this.wasPausedWhenOpened) {
|
||||
if (pause) {
|
||||
crazyGamesSDK.gameplayStop();
|
||||
} else {
|
||||
crazyGamesSDK.gameplayStart();
|
||||
}
|
||||
this.eventBus.emit(new PauseGameIntentEvent(pause));
|
||||
}
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("click", this.handleOutsideClick, true);
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("click", this.handleOutsideClick, true);
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private handleOutsideClick = (event: MouseEvent) => {
|
||||
if (
|
||||
this.isVisible &&
|
||||
this.modalOverlay &&
|
||||
event.target === this.modalOverlay
|
||||
) {
|
||||
this.closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
private handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (this.isVisible && event.key === "Escape") {
|
||||
this.closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
public closeModal() {
|
||||
this.isVisible = false;
|
||||
this.requestUpdate();
|
||||
this.pauseGame(false);
|
||||
}
|
||||
|
||||
private currentNameScale(): number {
|
||||
return (
|
||||
this.userSettings.graphicsOverrides().name?.nameScaleFactor ??
|
||||
renderDefaults.name.nameScaleFactor
|
||||
);
|
||||
}
|
||||
|
||||
private currentNameCull(): number {
|
||||
return (
|
||||
this.userSettings.graphicsOverrides().name?.cullThreshold ??
|
||||
renderDefaults.name.cullThreshold
|
||||
);
|
||||
}
|
||||
|
||||
private patchName(patch: Partial<GraphicsOverrides["name"]>) {
|
||||
const current = this.userSettings.graphicsOverrides();
|
||||
this.userSettings.setGraphicsOverrides({
|
||||
...current,
|
||||
name: { ...current.name, ...patch },
|
||||
});
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onNameScaleChange(event: Event) {
|
||||
const value = parseFloat((event.target as HTMLInputElement).value);
|
||||
this.patchName({ nameScaleFactor: value });
|
||||
}
|
||||
|
||||
private onNameCullChange(event: Event) {
|
||||
const value = parseFloat((event.target as HTMLInputElement).value);
|
||||
this.patchName({ cullThreshold: value });
|
||||
}
|
||||
|
||||
private onResetClick() {
|
||||
this.userSettings.setGraphicsOverrides({});
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) return null;
|
||||
|
||||
const nameScale = this.currentNameScale();
|
||||
const nameCull = this.currentNameCull();
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="modal-overlay fixed inset-0 bg-black/60 backdrop-blur-xs z-2000 flex items-center justify-center p-4"
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
class="bg-slate-800 border border-slate-600 rounded-lg max-w-md w-full max-h-[80vh] overflow-y-auto"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between p-4 border-b border-slate-600"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src=${settingsIcon}
|
||||
alt="graphicsSettings"
|
||||
width="24"
|
||||
height="24"
|
||||
class="align-middle"
|
||||
/>
|
||||
<h2 class="text-xl font-semibold text-white">
|
||||
${translateText("graphics_setting.title")}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
class="text-slate-400 hover:text-white text-2xl font-bold leading-none"
|
||||
@click=${this.closeModal}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-4 flex flex-col gap-3">
|
||||
<div
|
||||
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider"
|
||||
>
|
||||
${translateText("graphics_setting.section_name_labels")}
|
||||
</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.name_scale_label")}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min=${NAME_SCALE_MIN}
|
||||
max=${NAME_SCALE_MAX}
|
||||
step=${NAME_SCALE_STEP}
|
||||
.value=${String(nameScale)}
|
||||
@input=${this.onNameScaleChange}
|
||||
class="w-full border border-slate-500 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-slate-400 w-12 text-right">
|
||||
${nameScale.toFixed(2)}
|
||||
</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.name_cull_label")}
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${translateText("graphics_setting.name_cull_desc")}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min=${NAME_CULL_MIN}
|
||||
max=${NAME_CULL_MAX}
|
||||
step=${NAME_CULL_STEP}
|
||||
.value=${String(nameCull)}
|
||||
@input=${this.onNameCullChange}
|
||||
class="w-full border border-slate-500 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-slate-400 w-12 text-right">
|
||||
${nameCull.toFixed(3)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
@click=${this.onResetClick}
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">
|
||||
${translateText("graphics_setting.reset_label")}
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${translateText("graphics_setting.reset_desc")}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -83,9 +83,9 @@ export class HeadsUpMessage extends LitElement implements Controller {
|
||||
|
||||
tick() {
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
if (updates && updates[GameUpdateType.GamePaused].length > 0) {
|
||||
const pauseUpdate = updates[GameUpdateType.GamePaused][0];
|
||||
this.isPaused = pauseUpdate.paused;
|
||||
const pauseUpdates = updates?.[GameUpdateType.GamePaused];
|
||||
if (pauseUpdates && pauseUpdates.length > 0) {
|
||||
this.isPaused = pauseUpdates[pauseUpdates.length - 1].paused;
|
||||
}
|
||||
|
||||
const showImmunityHudDuration = 10 * 10;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SetBackgroundMusicVolumeEvent,
|
||||
SetSoundEffectsVolumeEvent,
|
||||
} from "../../sound/Sounds";
|
||||
import { ShowGraphicsSettingsModalEvent } from "./GraphicsSettingsModal";
|
||||
const structureIcon = assetUrl("images/CityIconWhite.svg");
|
||||
const cursorPriceIcon = assetUrl("images/CursorPriceIconWhite.svg");
|
||||
const darkModeIcon = assetUrl("images/DarkModeIconWhite.svg");
|
||||
@@ -104,10 +105,10 @@ export class SettingsModal extends LitElement implements Controller {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public closeModal() {
|
||||
public closeModal({ keepPause = false }: { keepPause?: boolean } = {}) {
|
||||
this.isVisible = false;
|
||||
this.requestUpdate();
|
||||
this.pauseGame(false);
|
||||
if (!keepPause) this.pauseGame(false);
|
||||
}
|
||||
|
||||
private pauseGame(pause: boolean) {
|
||||
@@ -183,6 +184,17 @@ export class SettingsModal extends LitElement implements Controller {
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
private onGraphicsSettingsButtonClick() {
|
||||
this.eventBus.emit(
|
||||
new ShowGraphicsSettingsModalEvent(
|
||||
true,
|
||||
this.shouldPause,
|
||||
this.wasPausedWhenOpened,
|
||||
),
|
||||
);
|
||||
this.closeModal({ keepPause: true });
|
||||
}
|
||||
|
||||
private onExitButtonClick() {
|
||||
// redirect to the home page
|
||||
window.location.href = "/";
|
||||
@@ -510,6 +522,26 @@ export class SettingsModal extends LitElement implements Controller {
|
||||
</div>
|
||||
</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.onGraphicsSettingsButtonClick}"
|
||||
>
|
||||
<img
|
||||
src=${settingsIcon}
|
||||
alt="graphicsSettings"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">
|
||||
${translateText("user_setting.graphics_settings_label")}
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${translateText("user_setting.graphics_settings_desc")}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="border-t border-slate-600 pt-3 mt-4">
|
||||
<div
|
||||
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const GraphicsOverridesSchema = z
|
||||
.object({
|
||||
name: z
|
||||
.object({
|
||||
nameScaleFactor: z.number(),
|
||||
cullThreshold: z.number(),
|
||||
})
|
||||
.partial(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
export type GraphicsOverrides = z.infer<typeof GraphicsOverridesSchema>;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GraphicsOverrides } from "./GraphicsOverrides";
|
||||
import defaults from "./render-settings.json";
|
||||
|
||||
export interface RenderSettings {
|
||||
@@ -260,6 +261,23 @@ export function createRenderSettings(): RenderSettings {
|
||||
return JSON.parse(JSON.stringify(defaults)) as RenderSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a fresh RenderSettings by layering user overrides on top of the
|
||||
* render-settings.json defaults. Pure — does not mutate any input.
|
||||
*/
|
||||
export function generateRenderSettings(
|
||||
overrides: GraphicsOverrides,
|
||||
): RenderSettings {
|
||||
const settings = createRenderSettings();
|
||||
if (overrides.name?.nameScaleFactor !== undefined) {
|
||||
settings.name.nameScaleFactor = overrides.name.nameScaleFactor;
|
||||
}
|
||||
if (overrides.name?.cullThreshold !== undefined) {
|
||||
settings.name.cullThreshold = overrides.name.cullThreshold;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
/** Dump current settings to a downloadable JSON file. */
|
||||
export function dumpSettings(settings: RenderSettings): void {
|
||||
const json = JSON.stringify(settings, null, 2);
|
||||
|
||||
@@ -9,8 +9,14 @@ export type {
|
||||
RadialMenuSelectEvent,
|
||||
} from "./Events";
|
||||
export { GameView } from "./GameView";
|
||||
export { GraphicsOverridesSchema } from "./GraphicsOverrides";
|
||||
export type { GraphicsOverrides } from "./GraphicsOverrides";
|
||||
export type { SpawnCenter } from "./passes/SpawnOverlayPass";
|
||||
export { createRenderSettings, dumpSettings } from "./RenderSettings";
|
||||
export {
|
||||
createRenderSettings,
|
||||
dumpSettings,
|
||||
generateRenderSettings,
|
||||
} from "./RenderSettings";
|
||||
export type { RenderSettings } from "./RenderSettings";
|
||||
export { deepAssign, deepDiff } from "./SettingsUtils";
|
||||
export { buildTerrainRGBA, getPaletteSize } from "./utils/ColorUtils";
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
GraphicsOverrides,
|
||||
GraphicsOverridesSchema,
|
||||
} from "../../client/render/gl/GraphicsOverrides";
|
||||
import { Cosmetics } from "../CosmeticSchemas";
|
||||
import { PlayerPattern } from "../Schemas";
|
||||
|
||||
@@ -53,6 +57,7 @@ export const COLOR_KEY = "settings.territoryColor";
|
||||
export const DARK_MODE_KEY = "settings.darkMode";
|
||||
export const PERFORMANCE_OVERLAY_KEY = "settings.performanceOverlay";
|
||||
export const KEYBINDS_KEY = "settings.keybinds";
|
||||
export const GRAPHICS_KEY = "settings.graphics";
|
||||
|
||||
export class UserSettings {
|
||||
private static cache = new Map<string, string | null>();
|
||||
@@ -354,6 +359,23 @@ export class UserSettings {
|
||||
this.setFloat("settings.attackRatio", value);
|
||||
}
|
||||
|
||||
// Returns {} if missing, unparseable, or fails schema validation.
|
||||
graphicsOverrides(): GraphicsOverrides {
|
||||
const raw = this.getString(GRAPHICS_KEY, "");
|
||||
if (!raw) return {};
|
||||
try {
|
||||
const parsed = GraphicsOverridesSchema.safeParse(JSON.parse(raw));
|
||||
if (parsed.success) return parsed.data;
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
setGraphicsOverrides(value: GraphicsOverrides): void {
|
||||
this.setString(GRAPHICS_KEY, JSON.stringify(value));
|
||||
}
|
||||
|
||||
// In case localStorage was manually edited to be invalid, return an empty object
|
||||
parsedUserKeybinds(): Record<string, any> {
|
||||
const raw = this.getString(KEYBINDS_KEY, "{}");
|
||||
|
||||
Reference in New Issue
Block a user