diff --git a/resources/lang/en.json b/resources/lang/en.json index fdeedbb4d..bf01844e0 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -611,7 +611,7 @@ "attack_ratio_down": "Decrease Attack Ratio", "attack_ratio_down_desc": "Decrease attack ratio by {amount}%", "attack_ratio_increment_label": "Attack Ratio Keybind Increment", - "attack_ratio_increment_desc": "How much the attack ratio keybinds change per press.", + "attack_ratio_increment_desc": "How much the attack ratio keybinds change per press/scroll.", "attack_keybinds": "Attack Keybinds", "boat_attack": "Boat Attack", "boat_attack_desc": "Send a boat attack to the tile under your cursor.", diff --git a/src/client/FlagInput.ts b/src/client/FlagInput.ts index edcdb06b1..db59a4eba 100644 --- a/src/client/FlagInput.ts +++ b/src/client/FlagInput.ts @@ -1,7 +1,11 @@ import { LitElement, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { FlagName } from "../core/Schemas"; -import { UserSettings } from "../core/game/UserSettings"; +import { + FLAG_KEY, + USER_SETTINGS_CHANGED_EVENT, + UserSettings, +} from "../core/game/UserSettings"; import { resolveFlagUrl } from "./Cosmetics"; import { translateText } from "./Utils"; @@ -41,7 +45,7 @@ export class FlagInput extends LitElement { super.connectedCallback(); this.flag = new UserSettings().getFlag() ?? ""; window.addEventListener( - "event:user-settings-changed:flag", + `${USER_SETTINGS_CHANGED_EVENT}:${FLAG_KEY}`, this.updateFlag as EventListener, ); } @@ -49,7 +53,7 @@ export class FlagInput extends LitElement { disconnectedCallback() { super.disconnectedCallback(); window.removeEventListener( - "event:user-settings-changed:flag", + `${USER_SETTINGS_CHANGED_EVENT}:${FLAG_KEY}`, this.updateFlag as EventListener, ); } diff --git a/src/client/Main.ts b/src/client/Main.ts index 222d944f3..4c15d5684 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -11,7 +11,11 @@ import { import { GameEnv } from "../core/configuration/Config"; import { getRuntimeClientServerConfig } from "../core/configuration/ConfigLoader"; import { GameType } from "../core/game/Game"; -import { UserSettings } from "../core/game/UserSettings"; +import { + DARK_MODE_KEY, + USER_SETTINGS_CHANGED_EVENT, + UserSettings, +} from "../core/game/UserSettings"; import "./AccountModal"; import { getUserMe } from "./Api"; import { userAuth } from "./Auth"; @@ -478,11 +482,23 @@ class Client { this.joinModal.eventBus = this.eventBus; } - if (this.userSettings.darkMode()) { - document.documentElement.classList.add("dark"); - } else { - document.documentElement.classList.remove("dark"); - } + 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) => { + const isDark = e.detail === "true"; + applyDarkMode(isDark); + }, + ); // Attempt to join lobby if (document.readyState === "loading") { diff --git a/src/client/PatternInput.ts b/src/client/PatternInput.ts index 46910c486..c38c8218f 100644 --- a/src/client/PatternInput.ts +++ b/src/client/PatternInput.ts @@ -1,5 +1,9 @@ import { LitElement, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; +import { + PATTERN_KEY, + USER_SETTINGS_CHANGED_EVENT, +} from "../core/game/UserSettings"; import { PlayerPattern } from "../core/Schemas"; import { renderPatternPreview } from "./components/PatternButton"; import { getPlayerCosmetics } from "./Cosmetics"; @@ -47,7 +51,7 @@ export class PatternInput extends LitElement { if (!this.isConnected) return; this.isLoading = false; window.addEventListener( - "event:user-settings-changed:pattern", + `${USER_SETTINGS_CHANGED_EVENT}:${PATTERN_KEY}`, this._onPatternSelected, { signal: this._abortController.signal, diff --git a/src/client/Store.ts b/src/client/Store.ts index b241345b7..2091a465e 100644 --- a/src/client/Store.ts +++ b/src/client/Store.ts @@ -3,7 +3,11 @@ import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { UserMeResponse } from "../core/ApiSchemas"; import { ColorPalette, Cosmetics, Pattern } from "../core/CosmeticSchemas"; -import { UserSettings } from "../core/game/UserSettings"; +import { + PATTERN_KEY, + USER_SETTINGS_CHANGED_EVENT, + UserSettings, +} from "../core/game/UserSettings"; import { PlayerPattern } from "../core/Schemas"; import { BaseModal } from "./components/BaseModal"; import "./components/FlagButton"; @@ -45,7 +49,7 @@ export class StoreModal extends BaseModal { }, ); window.addEventListener( - "event:user-settings-changed:pattern", + `${USER_SETTINGS_CHANGED_EVENT}:${PATTERN_KEY}`, this._onPatternSelected, ); } @@ -53,7 +57,7 @@ export class StoreModal extends BaseModal { disconnectedCallback() { super.disconnectedCallback(); window.removeEventListener( - "event:user-settings-changed:pattern", + `${USER_SETTINGS_CHANGED_EVENT}:${PATTERN_KEY}`, this._onPatternSelected, ); } diff --git a/src/client/TerritoryPatternsModal.ts b/src/client/TerritoryPatternsModal.ts index 050377e70..9e6537073 100644 --- a/src/client/TerritoryPatternsModal.ts +++ b/src/client/TerritoryPatternsModal.ts @@ -3,7 +3,11 @@ import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { UserMeResponse } from "../core/ApiSchemas"; import { Cosmetics, Pattern } from "../core/CosmeticSchemas"; -import { UserSettings } from "../core/game/UserSettings"; +import { + PATTERN_KEY, + USER_SETTINGS_CHANGED_EVENT, + UserSettings, +} from "../core/game/UserSettings"; import { PlayerPattern } from "../core/Schemas"; import { BaseModal } from "./components/BaseModal"; import "./components/NotLoggedInWarning"; @@ -42,7 +46,7 @@ export class TerritoryPatternsModal extends BaseModal { }, ); window.addEventListener( - "event:user-settings-changed:pattern", + `${USER_SETTINGS_CHANGED_EVENT}:${PATTERN_KEY}`, this._onPatternSelected, ); } @@ -50,7 +54,7 @@ export class TerritoryPatternsModal extends BaseModal { disconnectedCallback() { super.disconnectedCallback(); window.removeEventListener( - "event:user-settings-changed:pattern", + `${USER_SETTINGS_CHANGED_EVENT}:${PATTERN_KEY}`, this._onPatternSelected, ); } diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index dba7be0fb..9914c66d2 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -71,7 +71,7 @@ export class UserSettingModal extends BaseModal { } private loadKeybindsFromStorage() { - const savedKeybinds = localStorage.getItem("settings.keybinds"); + const savedKeybinds = this.userSettings.keybinds(); if (!savedKeybinds) return; try { @@ -199,7 +199,7 @@ export class UserSettingModal extends BaseModal { } this.keybinds = { ...this.keybinds, [action]: { value: value, key: key } }; - localStorage.setItem("settings.keybinds", JSON.stringify(this.keybinds)); + this.userSettings.setKeybinds(JSON.stringify(this.keybinds)); } private getKeyValue(action: string): string | undefined { @@ -251,101 +251,77 @@ export class UserSettingModal extends BaseModal { }, 5000); } - toggleDarkMode(e: CustomEvent<{ checked: boolean }>) { - const enabled = e.detail?.checked; + toggleDarkMode() { + this.userSettings.toggleDarkMode(); - if (typeof enabled !== "boolean") { - console.warn("Unexpected toggle event payload", e); - return; - } + console.log("🌙 Dark Mode:", this.userSettings.darkMode() ? "ON" : "OFF"); + } - this.userSettings.set("settings.darkMode", enabled); + private toggleEmojis() { + this.userSettings.toggleEmojis(); - if (enabled) { - document.documentElement.classList.add("dark"); - } else { - document.documentElement.classList.remove("dark"); - } + console.log("🤡 Emojis:", this.userSettings.emojis() ? "ON" : "OFF"); + } - this.dispatchEvent( - new CustomEvent("dark-mode-changed", { - detail: { darkMode: enabled }, - bubbles: true, - composed: true, - }), + private toggleAlertFrame() { + this.userSettings.toggleAlertFrame(); + + console.log( + "🚨 Alert frame:", + this.userSettings.alertFrame() ? "ON" : "OFF", ); - - console.log("🌙 Dark Mode:", enabled ? "ON" : "OFF"); } - private toggleEmojis(e: CustomEvent<{ checked: boolean }>) { - const enabled = e.detail?.checked; - if (typeof enabled !== "boolean") return; + private toggleFxLayer() { + this.userSettings.toggleFxLayer(); - this.userSettings.set("settings.emojis", enabled); - - console.log("🤡 Emojis:", enabled ? "ON" : "OFF"); + console.log( + "💥 Special effects:", + this.userSettings.fxLayer() ? "ON" : "OFF", + ); } - private toggleAlertFrame(e: CustomEvent<{ checked: boolean }>) { - const enabled = e.detail?.checked; - if (typeof enabled !== "boolean") return; + private toggleStructureSprites() { + this.userSettings.toggleStructureSprites(); - this.userSettings.set("settings.alertFrame", enabled); - - console.log("🚨 Alert frame:", enabled ? "ON" : "OFF"); + console.log( + "🏠 Structure sprites:", + this.userSettings.structureSprites() ? "ON" : "OFF", + ); } - private toggleFxLayer(e: CustomEvent<{ checked: boolean }>) { - const enabled = e.detail?.checked; - if (typeof enabled !== "boolean") return; + private toggleCursorCostLabel() { + this.userSettings.toggleCursorCostLabel(); - this.userSettings.set("settings.specialEffects", enabled); - - console.log("💥 Special effects:", enabled ? "ON" : "OFF"); + console.log( + "💰 Cursor build cost:", + this.userSettings.cursorCostLabel() ? "ON" : "OFF", + ); } - private toggleStructureSprites(e: CustomEvent<{ checked: boolean }>) { - const enabled = e.detail?.checked; - if (typeof enabled !== "boolean") return; + private toggleAnonymousNames() { + this.userSettings.toggleRandomName(); - this.userSettings.set("settings.structureSprites", enabled); - - console.log("🏠 Structure sprites:", enabled ? "ON" : "OFF"); + console.log( + "🙈 Anonymous Names:", + this.userSettings.anonymousNames() ? "ON" : "OFF", + ); } - private toggleCursorCostLabel(e: CustomEvent<{ checked: boolean }>) { - const enabled = e.detail?.checked; - if (typeof enabled !== "boolean") return; - - this.userSettings.set("settings.cursorCostLabel", enabled); - - console.log("💰 Cursor build cost:", enabled ? "ON" : "OFF"); + private toggleLobbyIdVisibility() { + this.userSettings.toggleLobbyIdVisibility(); + console.log( + "👁️ Hidden Lobby IDs:", + !this.userSettings.lobbyIdVisibility() ? "ON" : "OFF", + ); } - private toggleAnonymousNames(e: CustomEvent<{ checked: boolean }>) { - const enabled = e.detail?.checked; - if (typeof enabled !== "boolean") return; - - this.userSettings.set("settings.anonymousNames", enabled); - - console.log("🙈 Anonymous Names:", enabled ? "ON" : "OFF"); - } - - private toggleLobbyIdVisibility(e: CustomEvent<{ checked: boolean }>) { - const hideIds = e.detail?.checked; - if (typeof hideIds !== "boolean") return; - - this.userSettings.set("settings.lobbyIdVisibility", !hideIds); // Invert because checked=hide - console.log("👁️ Hidden Lobby IDs:", hideIds ? "ON" : "OFF"); - } - - private toggleLeftClickOpensMenu(e: CustomEvent<{ checked: boolean }>) { - const enabled = e.detail?.checked; - if (typeof enabled !== "boolean") return; - - this.userSettings.set("settings.leftClickOpensMenu", enabled); - console.log("🖱️ Left Click Opens Menu:", enabled ? "ON" : "OFF"); + private toggleLeftClickOpensMenu() { + this.userSettings.toggleLeftClickOpenMenu(); + console.log( + "🖱️ Left Click Opens Menu:", + this.userSettings.leftClickOpensMenu() ? "ON" : "OFF", + ); this.requestUpdate(); } @@ -354,7 +330,7 @@ export class UserSettingModal extends BaseModal { const value = e.detail?.value; if (typeof value === "number") { const ratio = value / 100; - localStorage.setItem("settings.attackRatio", ratio.toString()); + this.userSettings.setAttackRatio(ratio); } else { console.warn("Slider event missing detail.value", e); } @@ -370,27 +346,21 @@ export class UserSettingModal extends BaseModal { console.warn("Select event missing detail.value", e); return; } - this.userSettings.setFloat( - "settings.attackRatioIncrement", - Math.round(value), - ); + this.userSettings.setAttackRatioIncrement(Math.round(value)); this.requestUpdate(); } - private toggleTerritoryPatterns(e: CustomEvent<{ checked: boolean }>) { - const enabled = e.detail?.checked; - if (typeof enabled !== "boolean") return; + private toggleTerritoryPatterns() { + this.userSettings.toggleTerritoryPatterns(); - this.userSettings.set("settings.territoryPatterns", enabled); - - console.log("🏳️ Territory Patterns:", enabled ? "ON" : "OFF"); + console.log( + "🏳️ Territory Patterns:", + this.userSettings.territoryPatterns() ? "ON" : "OFF", + ); } - private togglePerformanceOverlay(e: CustomEvent<{ checked: boolean }>) { - const enabled = e.detail?.checked; - if (typeof enabled !== "boolean") return; - - this.userSettings.set("settings.performanceOverlay", enabled); + private togglePerformanceOverlay() { + this.userSettings.togglePerformanceOverlay(); } render() { @@ -809,8 +779,7 @@ export class UserSettingModal extends BaseModal { description="${translateText("user_setting.dark_mode_desc")}" id="dark-mode-toggle" .checked=${this.userSettings.darkMode()} - @change=${(e: CustomEvent<{ checked: boolean }>) => - this.toggleDarkMode(e)} + @change=${this.toggleDarkMode} > @@ -881,7 +850,7 @@ export class UserSettingModal extends BaseModal { label="${translateText("user_setting.lobby_id_visibility_label")}" description="${translateText("user_setting.lobby_id_visibility_desc")}" id="lobby-id-visibility-toggle" - .checked=${!this.userSettings.get("settings.lobbyIdVisibility", true)} + .checked=${!this.userSettings.lobbyIdVisibility()} @change=${this.toggleLobbyIdVisibility} > @@ -909,8 +878,7 @@ export class UserSettingModal extends BaseModal { description="${translateText("user_setting.attack_ratio_desc")}" min="1" max="100" - .value=${Number(localStorage.getItem("settings.attackRatio") ?? "0.2") * - 100} + .value=${this.userSettings.attackRatio() * 100} @change=${this.sliderAttackRatio} > diff --git a/src/client/components/CopyButton.ts b/src/client/components/CopyButton.ts index dea21f618..053d830ac 100644 --- a/src/client/components/CopyButton.ts +++ b/src/client/components/CopyButton.ts @@ -33,10 +33,7 @@ export class CopyButton extends LitElement { changedProperties: Map, ) { if (changedProperties.has("lobbyId")) { - this.lobbyIdVisible = this.userSettings.get( - "settings.lobbyIdVisibility", - true, - ); + this.lobbyIdVisible = this.userSettings.lobbyIdVisibility(); this.copySuccess = false; } if (changedProperties.has("copyText")) { diff --git a/src/client/components/baseComponents/setting/SettingToggle.ts b/src/client/components/baseComponents/setting/SettingToggle.ts index 30ed71e30..79dab8b97 100644 --- a/src/client/components/baseComponents/setting/SettingToggle.ts +++ b/src/client/components/baseComponents/setting/SettingToggle.ts @@ -16,13 +16,6 @@ export class SettingToggle extends LitElement { private handleChange(e: Event) { const input = e.target as HTMLInputElement; this.checked = input.checked; - this.dispatchEvent( - new CustomEvent("change", { - detail: { checked: this.checked }, - bubbles: true, - composed: true, - }), - ); } render() { diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 1937b4a3e..fad782e2c 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -273,7 +273,7 @@ export function createRenderer( // Not grouping the layers may cause excessive calls to context.save() and context.restore(). const layers: Layer[] = [ new TerrainLayer(game, transformHandler), - new TerritoryLayer(game, eventBus, transformHandler, userSettings), + new TerritoryLayer(game, eventBus, transformHandler), new RailroadLayer(game, eventBus, transformHandler, uiState), new CoordinateGridLayer(game, eventBus, transformHandler), structureLayer, diff --git a/src/client/graphics/layers/PerformanceOverlay.ts b/src/client/graphics/layers/PerformanceOverlay.ts index 9dd097ab9..98c1ba40c 100644 --- a/src/client/graphics/layers/PerformanceOverlay.ts +++ b/src/client/graphics/layers/PerformanceOverlay.ts @@ -1,7 +1,11 @@ import { LitElement, css, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { EventBus } from "../../../core/EventBus"; -import { UserSettings } from "../../../core/game/UserSettings"; +import { + PERFORMANCE_OVERLAY_KEY, + USER_SETTINGS_CHANGED_EVENT, + UserSettings, +} from "../../../core/game/UserSettings"; import { TickMetricsEvent, TogglePerformanceOverlayEvent, @@ -469,15 +473,15 @@ export class PerformanceOverlay extends LitElement implements Layer { ) => { const nextVisible = !this.isVisible; this.setVisible(nextVisible); - this.userSettings.set("settings.performanceOverlay", nextVisible); + this.userSettings.setPerformanceOverlay(nextVisible); }; private onTickMetricsEvent = (event: TickMetricsEvent) => { this.updateTickMetrics(event.tickExecutionDuration, event.tickDelay); }; - private onUserSettingsChanged = (event: CustomEvent) => { - const nextVisible = (event.detail as boolean) === true; + private onUserSettingsChanged = (event: CustomEvent) => { + const nextVisible = event.detail === "true"; if (this.isVisible === nextVisible) return; this.setVisible(nextVisible); }; @@ -505,7 +509,7 @@ export class PerformanceOverlay extends LitElement implements Layer { if (!this.isUserSettingsListenerAttached) { globalThis.addEventListener( - "event:user-settings-changed:settings.performanceOverlay", + `${USER_SETTINGS_CHANGED_EVENT}:${PERFORMANCE_OVERLAY_KEY}`, this.onUserSettingsChanged, ); this.isUserSettingsListenerAttached = true; @@ -517,7 +521,7 @@ export class PerformanceOverlay extends LitElement implements Layer { if (this.isUserSettingsListenerAttached) { globalThis.removeEventListener( - "event:user-settings-changed:settings.performanceOverlay", + `${USER_SETTINGS_CHANGED_EVENT}:${PERFORMANCE_OVERLAY_KEY}`, this.onUserSettingsChanged, ); this.isUserSettingsListenerAttached = false; @@ -576,7 +580,7 @@ export class PerformanceOverlay extends LitElement implements Layer { private handleClose() { const nextVisible = false; this.setVisible(nextVisible); - this.userSettings.set("settings.performanceOverlay", nextVisible); + this.userSettings.setPerformanceOverlay(nextVisible); } private onDragPointerMove = (e: PointerEvent) => { diff --git a/src/client/graphics/layers/TerrainLayer.ts b/src/client/graphics/layers/TerrainLayer.ts index 57efc759f..542d21612 100644 --- a/src/client/graphics/layers/TerrainLayer.ts +++ b/src/client/graphics/layers/TerrainLayer.ts @@ -1,4 +1,4 @@ -import { Theme } from "../../../core/configuration/Config"; +import { Config, Theme } from "../../../core/configuration/Config"; import { GameView } from "../../../core/game/GameView"; import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; @@ -8,16 +8,19 @@ export class TerrainLayer implements Layer { private context: CanvasRenderingContext2D; private imageData: ImageData; private theme: Theme; + private config: Config; constructor( private game: GameView, private transformHandler: TransformHandler, - ) {} + ) { + this.config = this.game.config(); + } shouldTransform(): boolean { return true; } tick() { - if (this.game.config().theme() !== this.theme) { + if (this.config.theme() !== this.theme) { this.redraw(); } } @@ -46,7 +49,7 @@ export class TerrainLayer implements Layer { } initImageData() { - this.theme = this.game.config().theme(); + this.theme = this.config.theme(); this.game.forEachTile((tile) => { const terrainColor = this.theme.terrainColor(this.game, tile); // TODO: isn't tileref and index the same? diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 08d3f5a9c..cc66b2eb9 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -12,7 +12,6 @@ import { import { euclDistFN, TileRef } from "../../../core/game/GameMap"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, PlayerView } from "../../../core/game/GameView"; -import { UserSettings } from "../../../core/game/UserSettings"; import { PseudoRandom } from "../../../core/PseudoRandom"; import { AlternateViewEvent, @@ -24,7 +23,6 @@ import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; export class TerritoryLayer implements Layer { - private userSettings: UserSettings; private canvas: HTMLCanvasElement; private context: CanvasRenderingContext2D; private imageData: ImageData; @@ -62,9 +60,7 @@ export class TerritoryLayer implements Layer { private game: GameView, private eventBus: EventBus, private transformHandler: TransformHandler, - userSettings: UserSettings, ) { - this.userSettings = userSettings; this.theme = game.config().theme(); this.cachedTerritoryPatternsEnabled = undefined; } diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts index d8edb3f02..79cebcb56 100644 --- a/src/client/graphics/layers/UILayer.ts +++ b/src/client/graphics/layers/UILayer.ts @@ -4,7 +4,6 @@ import { Theme } from "../../../core/configuration/Config"; import { UnitType } from "../../../core/game/Game"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, UnitView } from "../../../core/game/GameView"; -import { UserSettings } from "../../../core/game/UserSettings"; import { UnitSelectionEvent } from "../../InputHandler"; import { ProgressBar } from "../ProgressBar"; import { TransformHandler } from "../TransformHandler"; @@ -28,7 +27,6 @@ export class UILayer implements Layer { private canvas: HTMLCanvasElement; private context: CanvasRenderingContext2D | null; private theme: Theme | null = null; - private userSettings: UserSettings = new UserSettings(); private selectionAnimTime = 0; private allProgressBars: Map< number, diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 1bd4dd61c..da1b5f6bf 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -228,14 +228,10 @@ export class PlayerView { ); } - const defaultTerritoryColor = this.game - .config() - .theme() - .territoryColor(this); - const defaultBorderColor = this.game - .config() - .theme() - .borderColor(defaultTerritoryColor); + const theme = this.game.config().theme(); + + const defaultTerritoryColor = theme.territoryColor(this); + const defaultBorderColor = theme.borderColor(defaultTerritoryColor); const pattern = userSettings.territoryPatterns() ? this.cosmetics.pattern @@ -258,14 +254,11 @@ export class PlayerView { this._territoryColor = defaultTerritoryColor; } - this._structureColors = this.game - .config() - .theme() - .structureColors(this._territoryColor); + this._structureColors = theme.structureColors(this._territoryColor); const maybeFocusedBorderColor = this.game.myClientID() === this.data.clientID - ? this.game.config().theme().focusedBorderColor() + ? theme.focusedBorderColor() : defaultBorderColor; this._borderColor = new Colord( @@ -275,7 +268,6 @@ export class PlayerView { ); // Pre-compute all border color variants once - const theme = this.game.config().theme(); const baseRgb = this._borderColor.toRgb(); // Neutral is just the base color diff --git a/src/core/game/UserSettings.ts b/src/core/game/UserSettings.ts index de7be1c2a..33be61982 100644 --- a/src/core/game/UserSettings.ts +++ b/src/core/game/UserSettings.ts @@ -1,15 +1,22 @@ import { Cosmetics } from "../CosmeticSchemas"; import { PlayerPattern } from "../Schemas"; -const PATTERN_KEY = "territoryPattern"; +export const USER_SETTINGS_CHANGED_EVENT = "event:user-settings-changed"; +export const PATTERN_KEY = "territoryPattern"; +export const FLAG_KEY = "flag"; +export const COLOR_KEY = "settings.territoryColor"; +export const DARK_MODE_KEY = "settings.darkMode"; +export const PERFORMANCE_OVERLAY_KEY = "settings.performanceOverlay"; export class UserSettings { + private static cache = new Map(); + private emitChange(key: string, value: any): void { try { const maybeDispatch = (globalThis as any)?.dispatchEvent; if (typeof maybeDispatch !== "function") return; (globalThis as any).dispatchEvent( - new CustomEvent(`event:user-settings-changed:${key}`, { + new CustomEvent(`${USER_SETTINGS_CHANGED_EVENT}:${key}`, { detail: value, }), ); @@ -18,147 +25,167 @@ export class UserSettings { } } - get(key: string, defaultValue: boolean): boolean { - const value = localStorage.getItem(key); + private getCached(key: string): string | null { + if (!UserSettings.cache.has(key)) { + UserSettings.cache.set(key, localStorage.getItem(key)); + } + return UserSettings.cache.get(key) ?? null; + } + + private setCached(key: string, value: string, emitChange: boolean = true) { + localStorage.setItem(key, value); + UserSettings.cache.set(key, value); + if (emitChange) { + this.emitChange(key, value); + } + } + + private removeCached(key: string, emitChange: boolean = true) { + localStorage.removeItem(key); + UserSettings.cache.set(key, null); + if (emitChange) { + this.emitChange(key, null); + } + } + + private getBool(key: string, defaultValue: boolean): boolean { + const value = this.getCached(key); if (!value) return defaultValue; - if (value === "true") return true; - if (value === "false") return false; - return defaultValue; } - set(key: string, value: boolean) { - localStorage.setItem(key, value ? "true" : "false"); - this.emitChange(key, value); + private setBool(key: string, value: boolean) { + this.setCached(key, value ? "true" : "false"); } - getFloat(key: string, defaultValue: number): number { - const value = localStorage.getItem(key); + private getString(key: string, defaultValue: string = ""): string { + const value = this.getCached(key); + if (value === null) return defaultValue; + return value; + } + + private setString(key: string, value: string) { + this.setCached(key, value); + } + + private getFloat(key: string, defaultValue: number): number { + const value = this.getCached(key); if (!value) return defaultValue; const floatValue = parseFloat(value); if (isNaN(floatValue)) return defaultValue; - return floatValue; } - setFloat(key: string, value: number) { - localStorage.setItem(key, value.toString()); - this.emitChange(key, value); + private setFloat(key: string, value: number) { + this.setCached(key, value.toString()); } emojis() { - return this.get("settings.emojis", true); + return this.getBool("settings.emojis", true); } performanceOverlay() { - return this.get("settings.performanceOverlay", false); + return this.getBool(PERFORMANCE_OVERLAY_KEY, false); } alertFrame() { - return this.get("settings.alertFrame", true); + return this.getBool("settings.alertFrame", true); } anonymousNames() { - return this.get("settings.anonymousNames", false); + return this.getBool("settings.anonymousNames", false); } lobbyIdVisibility() { - return this.get("settings.lobbyIdVisibility", true); + return this.getBool("settings.lobbyIdVisibility", true); } fxLayer() { - return this.get("settings.specialEffects", true); + return this.getBool("settings.specialEffects", true); } structureSprites() { - return this.get("settings.structureSprites", true); + return this.getBool("settings.structureSprites", true); } darkMode() { - return this.get("settings.darkMode", false); + return this.getBool(DARK_MODE_KEY, false); } leftClickOpensMenu() { - return this.get("settings.leftClickOpensMenu", false); + return this.getBool("settings.leftClickOpensMenu", false); } territoryPatterns() { - return this.get("settings.territoryPatterns", true); + return this.getBool("settings.territoryPatterns", true); } attackingTroopsOverlay() { - return this.get("settings.attackingTroopsOverlay", true); + return this.getBool("settings.attackingTroopsOverlay", true); } toggleAttackingTroopsOverlay() { - this.set("settings.attackingTroopsOverlay", !this.attackingTroopsOverlay()); + this.setBool( + "settings.attackingTroopsOverlay", + !this.attackingTroopsOverlay(), + ); } cursorCostLabel() { - const legacy = this.get("settings.ghostPricePill", true); - return this.get("settings.cursorCostLabel", legacy); - } - - focusLocked() { - return false; - // TODO: re-enable when performance issues are fixed. - this.get("settings.focusLocked", true); + const legacy = this.getBool("settings.ghostPricePill", true); + return this.getBool("settings.cursorCostLabel", legacy); } toggleLeftClickOpenMenu() { - this.set("settings.leftClickOpensMenu", !this.leftClickOpensMenu()); - } - - toggleFocusLocked() { - this.set("settings.focusLocked", !this.focusLocked()); + this.setBool("settings.leftClickOpensMenu", !this.leftClickOpensMenu()); } toggleEmojis() { - this.set("settings.emojis", !this.emojis()); + this.setBool("settings.emojis", !this.emojis()); + } + + // Performance overlay specifically needs a direct setter for Shift-D + setPerformanceOverlay(value: boolean) { + this.setBool(PERFORMANCE_OVERLAY_KEY, value); } togglePerformanceOverlay() { - this.set("settings.performanceOverlay", !this.performanceOverlay()); + this.setBool(PERFORMANCE_OVERLAY_KEY, !this.performanceOverlay()); } toggleAlertFrame() { - this.set("settings.alertFrame", !this.alertFrame()); + this.setBool("settings.alertFrame", !this.alertFrame()); } toggleRandomName() { - this.set("settings.anonymousNames", !this.anonymousNames()); + this.setBool("settings.anonymousNames", !this.anonymousNames()); } toggleLobbyIdVisibility() { - this.set("settings.lobbyIdVisibility", !this.lobbyIdVisibility()); + this.setBool("settings.lobbyIdVisibility", !this.lobbyIdVisibility()); } toggleFxLayer() { - this.set("settings.specialEffects", !this.fxLayer()); + this.setBool("settings.specialEffects", !this.fxLayer()); } toggleStructureSprites() { - this.set("settings.structureSprites", !this.structureSprites()); + this.setBool("settings.structureSprites", !this.structureSprites()); } toggleCursorCostLabel() { - this.set("settings.cursorCostLabel", !this.cursorCostLabel()); + this.setBool("settings.cursorCostLabel", !this.cursorCostLabel()); } toggleTerritoryPatterns() { - this.set("settings.territoryPatterns", !this.territoryPatterns()); + this.setBool("settings.territoryPatterns", !this.territoryPatterns()); } toggleDarkMode() { - this.set("settings.darkMode", !this.darkMode()); - if (this.darkMode()) { - document.documentElement.classList.add("dark"); - } else { - document.documentElement.classList.remove("dark"); - } + this.setBool(DARK_MODE_KEY, !this.darkMode()); } // For development only. Used for testing patterns, set in the console manually. @@ -178,7 +205,7 @@ export class UserSettings { getSelectedPatternName(cosmetics: Cosmetics | null): PlayerPattern | null { if (cosmetics === null) return null; - let data = localStorage.getItem(PATTERN_KEY) ?? null; + let data = this.getCached(PATTERN_KEY); if (data === null) return null; const patternPrefix = "pattern:"; if (data.startsWith(patternPrefix)) { @@ -196,34 +223,32 @@ export class UserSettings { setSelectedPatternName(patternName: string | undefined): void { if (patternName === undefined) { - localStorage.removeItem(PATTERN_KEY); + this.removeCached(PATTERN_KEY); } else { - localStorage.setItem(PATTERN_KEY, patternName); + this.setCached(PATTERN_KEY, patternName); } - this.emitChange("pattern", patternName); } getSelectedColor(): string | undefined { - const data = localStorage.getItem("settings.territoryColor") ?? undefined; - if (data === undefined) return undefined; - return data; + return this.getCached(COLOR_KEY) ?? undefined; } setSelectedColor(color: string | undefined): void { if (color === undefined) { - localStorage.removeItem("settings.territoryColor"); + this.removeCached(COLOR_KEY); } else { - localStorage.setItem("settings.territoryColor", color); + this.setCached(COLOR_KEY, color); } } getFlag(): string | null { - let flag = localStorage.getItem("flag"); + let flag = this.getCached(FLAG_KEY); if (!flag) return null; // Migrate bare country codes to country: prefix if (!flag.startsWith("flag:") && !flag.startsWith("country:")) { flag = `country:${flag}`; - localStorage.setItem("flag", flag); + // Silent migration: don't emit change event for FlagInput + this.setCached(FLAG_KEY, flag, false); } return flag; } @@ -232,14 +257,12 @@ export class UserSettings { if (flag === "country:xx") { this.clearFlag(); } else { - localStorage.setItem("flag", flag); + this.setCached(FLAG_KEY, flag); } - console.log("emitting change!"); - this.emitChange("flag", flag); } clearFlag(): void { - localStorage.removeItem("flag"); + this.removeCached(FLAG_KEY); } backgroundMusicVolume(): number { @@ -250,6 +273,7 @@ export class UserSettings { this.setFloat("settings.backgroundMusicVolume", volume); } + // What % attack ratio increments per click/scroll attackRatioIncrement(): number { const increment = Math.round( this.getFloat("settings.attackRatioIncrement", 10), @@ -258,6 +282,27 @@ export class UserSettings { return increment; } + setAttackRatioIncrement(value: number): void { + this.setFloat("settings.attackRatioIncrement", value); + } + + // What % attack ratio is set to + attackRatio(): number { + return this.getFloat("settings.attackRatio", 0.2); + } + + setAttackRatio(value: number): void { + this.setFloat("settings.attackRatio", value); + } + + keybinds(): string { + return this.getString("settings.keybinds", ""); + } + + setKeybinds(value: string): void { + this.setString("settings.keybinds", value); + } + soundEffectsVolume(): number { return this.getFloat("settings.soundEffectsVolume", 1); }