From 5b9ddd01260978c1d7fa67329d8a5432b1a9171b Mon Sep 17 00:00:00 2001 From: evanpelle Date: Thu, 28 May 2026 09:57:33 -0700 Subject: [PATCH 1/2] graphics --- index.html | 1 + resources/lang/en.json | 11 + src/client/ClientGameRunner.ts | 20 +- src/client/hud/GameRenderer.ts | 11 + .../hud/layers/GraphicsSettingsModal.ts | 242 ++++++++++++++++++ src/client/hud/layers/SettingsModal.ts | 26 ++ src/client/render/gl/RenderSettings.ts | 18 ++ src/client/render/gl/index.ts | 6 +- src/core/game/UserSettings.ts | 22 ++ 9 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 src/client/hud/layers/GraphicsSettingsModal.ts diff --git a/index.html b/index.html index d2e4e9270d..4af07ad2d6 100644 --- a/index.html +++ b/index.html @@ -341,6 +341,7 @@ + diff --git a/resources/lang/en.json b/resources/lang/en.json index a9eeffd3eb..da3ae5706e 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -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}", diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 3077065b30..c615464060 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -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,19 @@ async function createClientGame( (e) => view.setShowPatterns((e as CustomEvent).detail === "true"), ); + 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, + ); + let debugGui: ReturnType | null = null; eventBus.on(ToggleRenderDebugGuiEvent, () => { if (debugGui === null) { diff --git a/src/client/hud/GameRenderer.ts b/src/client/hud/GameRenderer.ts index eb9cb39447..3c08c477c9 100644 --- a/src/client/hud/GameRenderer.ts +++ b/src/client/hud/GameRenderer.ts @@ -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, diff --git a/src/client/hud/layers/GraphicsSettingsModal.ts b/src/client/hud/layers/GraphicsSettingsModal.ts new file mode 100644 index 0000000000..3b62b55b19 --- /dev/null +++ b/src/client/hud/layers/GraphicsSettingsModal.ts @@ -0,0 +1,242 @@ +import { html, LitElement } from "lit"; +import { customElement, query, state } from "lit/decorators.js"; +import { z } from "zod"; +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 renderDefaults from "../../render/gl/render-settings.json"; + +const settingsIcon = assetUrl("images/SettingIconWhite.svg"); + +export const GraphicsOverridesSchema = z + .object({ + name: z + .object({ + nameScaleFactor: z.number(), + cullThreshold: z.number(), + }) + .partial(), + }) + .partial(); + +export type GraphicsOverrides = z.infer; + +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) {} +} + +@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; + + init() { + this.eventBus.on(ShowGraphicsSettingsModalEvent, (event) => { + this.isVisible = event.isVisible; + this.requestUpdate(); + }); + } + + 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(); + } + + 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) { + 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` + + `; + } +} diff --git a/src/client/hud/layers/SettingsModal.ts b/src/client/hud/layers/SettingsModal.ts index bd9bf48210..17342e75ef 100644 --- a/src/client/hud/layers/SettingsModal.ts +++ b/src/client/hud/layers/SettingsModal.ts @@ -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"); @@ -183,6 +184,11 @@ export class SettingsModal extends LitElement implements Controller { this.closeModal(); } + private onGraphicsSettingsButtonClick() { + this.eventBus.emit(new ShowGraphicsSettingsModalEvent()); + this.closeModal(); + } + private onExitButtonClick() { // redirect to the home page window.location.href = "/"; @@ -510,6 +516,26 @@ export class SettingsModal extends LitElement implements Controller { + +
(); @@ -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 { const raw = this.getString(KEYBINDS_KEY, "{}"); From 7c6af87be56a70744cf7a9c3a52c3748b241f00a Mon Sep 17 00:00:00 2001 From: evanpelle Date: Thu, 28 May 2026 11:33:37 -0700 Subject: [PATCH 2/2] fixes --- src/client/ClientGameRunner.ts | 5 ++ .../hud/layers/GraphicsSettingsModal.ts | 46 ++++++++++++------- src/client/hud/layers/HeadsUpMessage.ts | 6 +-- src/client/hud/layers/SettingsModal.ts | 14 ++++-- src/client/render/gl/GraphicsOverrides.ts | 14 ++++++ src/client/render/gl/RenderSettings.ts | 2 +- src/client/render/gl/index.ts | 2 + src/core/game/UserSettings.ts | 2 +- 8 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 src/client/render/gl/GraphicsOverrides.ts diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index c615464060..e54ea553b2 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -484,6 +484,7 @@ async function createClientGame( (e) => view.setShowPatterns((e as CustomEvent).detail === "true"), ); + const graphicsListenerAbort = new AbortController(); const applyGraphicsOverrides = (): void => { const generated = generateRenderSettings( userSettings.graphicsOverrides(), @@ -495,6 +496,7 @@ async function createClientGame( globalThis.addEventListener( `${USER_SETTINGS_CHANGED_EVENT}:${GRAPHICS_KEY}`, applyGraphicsOverrides, + { signal: graphicsListenerAbort.signal }, ); let debugGui: ReturnType | null = null; @@ -542,6 +544,7 @@ async function createClientGame( soundManager, userSettings, webglBuilder, + graphicsListenerAbort, ); } catch (err) { soundManager.dispose(); @@ -575,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(); } @@ -831,6 +835,7 @@ export class ClientGameRunner { public stop() { this.soundManager.dispose(); + this.graphicsListenerAbort?.abort(); if (!this.isActive) return; this.isActive = false; diff --git a/src/client/hud/layers/GraphicsSettingsModal.ts b/src/client/hud/layers/GraphicsSettingsModal.ts index 3b62b55b19..969691edb8 100644 --- a/src/client/hud/layers/GraphicsSettingsModal.ts +++ b/src/client/hud/layers/GraphicsSettingsModal.ts @@ -1,28 +1,17 @@ import { html, LitElement } from "lit"; -import { customElement, query, state } from "lit/decorators.js"; -import { z } from "zod"; +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"); -export const GraphicsOverridesSchema = z - .object({ - name: z - .object({ - nameScaleFactor: z.number(), - cullThreshold: z.number(), - }) - .partial(), - }) - .partial(); - -export type GraphicsOverrides = z.infer; - const NAME_SCALE_MIN = 0.2; const NAME_SCALE_MAX = 1.5; const NAME_SCALE_STEP = 0.05; @@ -32,7 +21,11 @@ const NAME_CULL_MAX = 0.05; const NAME_CULL_STEP = 0.001; export class ShowGraphicsSettingsModalEvent { - constructor(public readonly isVisible: boolean = true) {} + constructor( + public readonly isVisible: boolean = true, + public readonly shouldPause: boolean = false, + public readonly isPaused: boolean = false, + ) {} } @customElement("graphics-settings-modal") @@ -46,13 +39,33 @@ export class GraphicsSettingsModal extends LitElement implements Controller { @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; } @@ -88,6 +101,7 @@ export class GraphicsSettingsModal extends LitElement implements Controller { public closeModal() { this.isVisible = false; this.requestUpdate(); + this.pauseGame(false); } private currentNameScale(): number { diff --git a/src/client/hud/layers/HeadsUpMessage.ts b/src/client/hud/layers/HeadsUpMessage.ts index 7f9b95532d..d7970ea320 100644 --- a/src/client/hud/layers/HeadsUpMessage.ts +++ b/src/client/hud/layers/HeadsUpMessage.ts @@ -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; diff --git a/src/client/hud/layers/SettingsModal.ts b/src/client/hud/layers/SettingsModal.ts index 17342e75ef..7b1be3d8aa 100644 --- a/src/client/hud/layers/SettingsModal.ts +++ b/src/client/hud/layers/SettingsModal.ts @@ -105,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) { @@ -185,8 +185,14 @@ export class SettingsModal extends LitElement implements Controller { } private onGraphicsSettingsButtonClick() { - this.eventBus.emit(new ShowGraphicsSettingsModalEvent()); - this.closeModal(); + this.eventBus.emit( + new ShowGraphicsSettingsModalEvent( + true, + this.shouldPause, + this.wasPausedWhenOpened, + ), + ); + this.closeModal({ keepPause: true }); } private onExitButtonClick() { diff --git a/src/client/render/gl/GraphicsOverrides.ts b/src/client/render/gl/GraphicsOverrides.ts new file mode 100644 index 0000000000..54ef6a3dd6 --- /dev/null +++ b/src/client/render/gl/GraphicsOverrides.ts @@ -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; diff --git a/src/client/render/gl/RenderSettings.ts b/src/client/render/gl/RenderSettings.ts index f3485ab014..4d2af1bedb 100644 --- a/src/client/render/gl/RenderSettings.ts +++ b/src/client/render/gl/RenderSettings.ts @@ -1,4 +1,4 @@ -import { GraphicsOverrides } from "../../hud/layers/GraphicsSettingsModal"; +import { GraphicsOverrides } from "./GraphicsOverrides"; import defaults from "./render-settings.json"; export interface RenderSettings { diff --git a/src/client/render/gl/index.ts b/src/client/render/gl/index.ts index fffc6de625..45c8ed4971 100644 --- a/src/client/render/gl/index.ts +++ b/src/client/render/gl/index.ts @@ -9,6 +9,8 @@ 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, diff --git a/src/core/game/UserSettings.ts b/src/core/game/UserSettings.ts index 8e6956a02a..133747b172 100644 --- a/src/core/game/UserSettings.ts +++ b/src/core/game/UserSettings.ts @@ -1,7 +1,7 @@ import { GraphicsOverrides, GraphicsOverridesSchema, -} from "../../client/hud/layers/GraphicsSettingsModal"; +} from "../../client/render/gl/GraphicsOverrides"; import { Cosmetics } from "../CosmeticSchemas"; import { PlayerPattern } from "../Schemas";