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>
|
<replay-panel></replay-panel>
|
||||||
</div>
|
</div>
|
||||||
<settings-modal></settings-modal>
|
<settings-modal></settings-modal>
|
||||||
|
<graphics-settings-modal></graphics-settings-modal>
|
||||||
<player-panel></player-panel>
|
<player-panel></player-panel>
|
||||||
<spawn-timer></spawn-timer>
|
<spawn-timer></spawn-timer>
|
||||||
<immunity-timer></immunity-timer>
|
<immunity-timer></immunity-timer>
|
||||||
|
|||||||
@@ -827,6 +827,8 @@
|
|||||||
"render_debug_gui": "Render Debug GUI",
|
"render_debug_gui": "Render Debug GUI",
|
||||||
"render_debug_gui_desc": "Toggle the renderer tuning panel",
|
"render_debug_gui_desc": "Toggle the renderer tuning panel",
|
||||||
"development_only": "Development Only",
|
"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_label": "Writing Speed Multiplier",
|
||||||
"easter_writing_speed_desc": "Adjust how fast you pretend to code (x1–x100)",
|
"easter_writing_speed_desc": "Adjust how fast you pretend to code (x1–x100)",
|
||||||
"easter_bug_count_label": "Bug Count",
|
"easter_bug_count_label": "Bug Count",
|
||||||
@@ -915,6 +917,15 @@
|
|||||||
"sound_effects_volume": "Sound Effects Volume",
|
"sound_effects_volume": "Sound Effects Volume",
|
||||||
"keybind_conflict_error": "The key {key} is already bound to another action."
|
"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": {
|
"chat": {
|
||||||
"title": "Quick Chat",
|
"title": "Quick Chat",
|
||||||
"to": "Sent {user}: {msg}",
|
"to": "Sent {user}: {msg}",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { GameView, PlayerView } from "../core/game/GameView";
|
|||||||
import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader";
|
import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader";
|
||||||
import {
|
import {
|
||||||
DARK_MODE_KEY,
|
DARK_MODE_KEY,
|
||||||
|
GRAPHICS_KEY,
|
||||||
USER_SETTINGS_CHANGED_EVENT,
|
USER_SETTINGS_CHANGED_EVENT,
|
||||||
UserSettings,
|
UserSettings,
|
||||||
} from "../core/game/UserSettings";
|
} from "../core/game/UserSettings";
|
||||||
@@ -67,7 +68,11 @@ import {
|
|||||||
import { createCanvas } from "./Utils";
|
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 { 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 { ALL_UNIT_TYPES, UnitState } from "./render/types";
|
||||||
import { SoundManager } from "./sound/SoundManager";
|
import { SoundManager } from "./sound/SoundManager";
|
||||||
|
|
||||||
@@ -479,6 +484,21 @@ async function createClientGame(
|
|||||||
(e) => view.setShowPatterns((e as CustomEvent<string>).detail === "true"),
|
(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;
|
let debugGui: ReturnType<typeof createDebugGui> | null = null;
|
||||||
eventBus.on(ToggleRenderDebugGuiEvent, () => {
|
eventBus.on(ToggleRenderDebugGuiEvent, () => {
|
||||||
if (debugGui === null) {
|
if (debugGui === null) {
|
||||||
@@ -524,6 +544,7 @@ async function createClientGame(
|
|||||||
soundManager,
|
soundManager,
|
||||||
userSettings,
|
userSettings,
|
||||||
webglBuilder,
|
webglBuilder,
|
||||||
|
graphicsListenerAbort,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
soundManager.dispose();
|
soundManager.dispose();
|
||||||
@@ -557,6 +578,7 @@ export class ClientGameRunner {
|
|||||||
private soundManager: SoundManager,
|
private soundManager: SoundManager,
|
||||||
private userSettings: UserSettings,
|
private userSettings: UserSettings,
|
||||||
private webglBuilder: WebGLFrameBuilder | null = null,
|
private webglBuilder: WebGLFrameBuilder | null = null,
|
||||||
|
private graphicsListenerAbort: AbortController | null = null,
|
||||||
) {
|
) {
|
||||||
this.lastMessageTime = Date.now();
|
this.lastMessageTime = Date.now();
|
||||||
}
|
}
|
||||||
@@ -813,6 +835,7 @@ export class ClientGameRunner {
|
|||||||
|
|
||||||
public stop() {
|
public stop() {
|
||||||
this.soundManager.dispose();
|
this.soundManager.dispose();
|
||||||
|
this.graphicsListenerAbort?.abort();
|
||||||
if (!this.isActive) return;
|
if (!this.isActive) return;
|
||||||
|
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { EmojiTable } from "./layers/EmojiTable";
|
|||||||
import { EventsDisplay } from "./layers/EventsDisplay";
|
import { EventsDisplay } from "./layers/EventsDisplay";
|
||||||
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
|
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
|
||||||
import { GameRightSidebar } from "./layers/GameRightSidebar";
|
import { GameRightSidebar } from "./layers/GameRightSidebar";
|
||||||
|
import { GraphicsSettingsModal } from "./layers/GraphicsSettingsModal";
|
||||||
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
|
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
|
||||||
import { ImmunityTimer } from "./layers/ImmunityTimer";
|
import { ImmunityTimer } from "./layers/ImmunityTimer";
|
||||||
import { InGamePromo } from "./layers/InGamePromo";
|
import { InGamePromo } from "./layers/InGamePromo";
|
||||||
@@ -190,6 +191,15 @@ export function createRenderer(
|
|||||||
settingsModal.userSettings = userSettings;
|
settingsModal.userSettings = userSettings;
|
||||||
settingsModal.eventBus = eventBus;
|
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;
|
const unitDisplay = document.querySelector("unit-display") as UnitDisplay;
|
||||||
if (!(unitDisplay instanceof UnitDisplay)) {
|
if (!(unitDisplay instanceof UnitDisplay)) {
|
||||||
console.error("unit display not found");
|
console.error("unit display not found");
|
||||||
@@ -309,6 +319,7 @@ export function createRenderer(
|
|||||||
winModal,
|
winModal,
|
||||||
replayPanel,
|
replayPanel,
|
||||||
settingsModal,
|
settingsModal,
|
||||||
|
graphicsSettingsModal,
|
||||||
teamStats,
|
teamStats,
|
||||||
playerPanel,
|
playerPanel,
|
||||||
headsUpMessage,
|
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() {
|
tick() {
|
||||||
const updates = this.game.updatesSinceLastTick();
|
const updates = this.game.updatesSinceLastTick();
|
||||||
if (updates && updates[GameUpdateType.GamePaused].length > 0) {
|
const pauseUpdates = updates?.[GameUpdateType.GamePaused];
|
||||||
const pauseUpdate = updates[GameUpdateType.GamePaused][0];
|
if (pauseUpdates && pauseUpdates.length > 0) {
|
||||||
this.isPaused = pauseUpdate.paused;
|
this.isPaused = pauseUpdates[pauseUpdates.length - 1].paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showImmunityHudDuration = 10 * 10;
|
const showImmunityHudDuration = 10 * 10;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
SetBackgroundMusicVolumeEvent,
|
SetBackgroundMusicVolumeEvent,
|
||||||
SetSoundEffectsVolumeEvent,
|
SetSoundEffectsVolumeEvent,
|
||||||
} from "../../sound/Sounds";
|
} from "../../sound/Sounds";
|
||||||
|
import { ShowGraphicsSettingsModalEvent } from "./GraphicsSettingsModal";
|
||||||
const structureIcon = assetUrl("images/CityIconWhite.svg");
|
const structureIcon = assetUrl("images/CityIconWhite.svg");
|
||||||
const cursorPriceIcon = assetUrl("images/CursorPriceIconWhite.svg");
|
const cursorPriceIcon = assetUrl("images/CursorPriceIconWhite.svg");
|
||||||
const darkModeIcon = assetUrl("images/DarkModeIconWhite.svg");
|
const darkModeIcon = assetUrl("images/DarkModeIconWhite.svg");
|
||||||
@@ -104,10 +105,10 @@ export class SettingsModal extends LitElement implements Controller {
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeModal() {
|
public closeModal({ keepPause = false }: { keepPause?: boolean } = {}) {
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
this.pauseGame(false);
|
if (!keepPause) this.pauseGame(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private pauseGame(pause: boolean) {
|
private pauseGame(pause: boolean) {
|
||||||
@@ -183,6 +184,17 @@ export class SettingsModal extends LitElement implements Controller {
|
|||||||
this.closeModal();
|
this.closeModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onGraphicsSettingsButtonClick() {
|
||||||
|
this.eventBus.emit(
|
||||||
|
new ShowGraphicsSettingsModalEvent(
|
||||||
|
true,
|
||||||
|
this.shouldPause,
|
||||||
|
this.wasPausedWhenOpened,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.closeModal({ keepPause: true });
|
||||||
|
}
|
||||||
|
|
||||||
private onExitButtonClick() {
|
private onExitButtonClick() {
|
||||||
// redirect to the home page
|
// redirect to the home page
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
@@ -510,6 +522,26 @@ 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.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="border-t border-slate-600 pt-3 mt-4">
|
||||||
<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"
|
||||||
|
|||||||
@@ -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";
|
import defaults from "./render-settings.json";
|
||||||
|
|
||||||
export interface RenderSettings {
|
export interface RenderSettings {
|
||||||
@@ -260,6 +261,23 @@ export function createRenderSettings(): RenderSettings {
|
|||||||
return JSON.parse(JSON.stringify(defaults)) as 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. */
|
/** Dump current settings to a downloadable JSON file. */
|
||||||
export function dumpSettings(settings: RenderSettings): void {
|
export function dumpSettings(settings: RenderSettings): void {
|
||||||
const json = JSON.stringify(settings, null, 2);
|
const json = JSON.stringify(settings, null, 2);
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ export type {
|
|||||||
RadialMenuSelectEvent,
|
RadialMenuSelectEvent,
|
||||||
} from "./Events";
|
} from "./Events";
|
||||||
export { GameView } from "./GameView";
|
export { GameView } from "./GameView";
|
||||||
|
export { GraphicsOverridesSchema } from "./GraphicsOverrides";
|
||||||
|
export type { GraphicsOverrides } from "./GraphicsOverrides";
|
||||||
export type { SpawnCenter } from "./passes/SpawnOverlayPass";
|
export type { SpawnCenter } from "./passes/SpawnOverlayPass";
|
||||||
export { createRenderSettings, dumpSettings } from "./RenderSettings";
|
export {
|
||||||
|
createRenderSettings,
|
||||||
|
dumpSettings,
|
||||||
|
generateRenderSettings,
|
||||||
|
} from "./RenderSettings";
|
||||||
export type { RenderSettings } from "./RenderSettings";
|
export type { RenderSettings } from "./RenderSettings";
|
||||||
export { deepAssign, deepDiff } from "./SettingsUtils";
|
export { deepAssign, deepDiff } from "./SettingsUtils";
|
||||||
export { buildTerrainRGBA, getPaletteSize } from "./utils/ColorUtils";
|
export { buildTerrainRGBA, getPaletteSize } from "./utils/ColorUtils";
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
GraphicsOverrides,
|
||||||
|
GraphicsOverridesSchema,
|
||||||
|
} from "../../client/render/gl/GraphicsOverrides";
|
||||||
import { Cosmetics } from "../CosmeticSchemas";
|
import { Cosmetics } from "../CosmeticSchemas";
|
||||||
import { PlayerPattern } from "../Schemas";
|
import { PlayerPattern } from "../Schemas";
|
||||||
|
|
||||||
@@ -53,6 +57,7 @@ export const COLOR_KEY = "settings.territoryColor";
|
|||||||
export const DARK_MODE_KEY = "settings.darkMode";
|
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 class UserSettings {
|
export class UserSettings {
|
||||||
private static cache = new Map<string, string | null>();
|
private static cache = new Map<string, string | null>();
|
||||||
@@ -354,6 +359,23 @@ export class UserSettings {
|
|||||||
this.setFloat("settings.attackRatio", value);
|
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
|
// In case localStorage was manually edited to be invalid, return an empty object
|
||||||
parsedUserKeybinds(): Record<string, any> {
|
parsedUserKeybinds(): Record<string, any> {
|
||||||
const raw = this.getString(KEYBINDS_KEY, "{}");
|
const raw = this.getString(KEYBINDS_KEY, "{}");
|
||||||
|
|||||||
Reference in New Issue
Block a user