import { Cosmetics } from "../CosmeticSchemas"; import { PlayerPattern } from "../Schemas"; const PATTERN_KEY = "territoryPattern"; export class UserSettings { 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}`, { detail: value, }), ); } catch { // Ignore - settings should still be applied even if event dispatch fails. } } get(key: string, defaultValue: boolean): boolean { const value = localStorage.getItem(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); } getFloat(key: string, defaultValue: number): number { const value = localStorage.getItem(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); } emojis() { return this.get("settings.emojis", true); } performanceOverlay() { return this.get("settings.performanceOverlay", false); } alertFrame() { return this.get("settings.alertFrame", true); } anonymousNames() { return this.get("settings.anonymousNames", false); } lobbyIdVisibility() { return this.get("settings.lobbyIdVisibility", true); } fxLayer() { return this.get("settings.specialEffects", true); } structureSprites() { return this.get("settings.structureSprites", true); } darkMode() { return this.get("settings.darkMode", false); } leftClickOpensMenu() { return this.get("settings.leftClickOpensMenu", false); } territoryPatterns() { return this.get("settings.territoryPatterns", true); } attackingTroopsOverlay() { return this.get("settings.attackingTroopsOverlay", true); } toggleAttackingTroopsOverlay() { this.set("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); } toggleLeftClickOpenMenu() { this.set("settings.leftClickOpensMenu", !this.leftClickOpensMenu()); } toggleFocusLocked() { this.set("settings.focusLocked", !this.focusLocked()); } toggleEmojis() { this.set("settings.emojis", !this.emojis()); } togglePerformanceOverlay() { this.set("settings.performanceOverlay", !this.performanceOverlay()); } toggleAlertFrame() { this.set("settings.alertFrame", !this.alertFrame()); } toggleRandomName() { this.set("settings.anonymousNames", !this.anonymousNames()); } toggleLobbyIdVisibility() { this.set("settings.lobbyIdVisibility", !this.lobbyIdVisibility()); } toggleFxLayer() { this.set("settings.specialEffects", !this.fxLayer()); } toggleStructureSprites() { this.set("settings.structureSprites", !this.structureSprites()); } toggleCursorCostLabel() { this.set("settings.cursorCostLabel", !this.cursorCostLabel()); } toggleTerritoryPatterns() { this.set("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"); } } // For development only. Used for testing patterns, set in the console manually. getDevOnlyPattern(): PlayerPattern | undefined { const data = localStorage.getItem("dev-pattern") ?? undefined; if (data === undefined) return undefined; return { name: "dev-pattern", patternData: data, colorPalette: { name: "dev-color-palette", primaryColor: localStorage.getItem("dev-primary") ?? "#ffffff", secondaryColor: localStorage.getItem("dev-secondary") ?? "#000000", }, } satisfies PlayerPattern; } getSelectedPatternName(cosmetics: Cosmetics | null): PlayerPattern | null { if (cosmetics === null) return null; let data = localStorage.getItem(PATTERN_KEY) ?? null; if (data === null) return null; const patternPrefix = "pattern:"; if (data.startsWith(patternPrefix)) { data = data.slice(patternPrefix.length); } const [patternName, colorPalette] = data.split(":"); const pattern = cosmetics.patterns[patternName]; if (pattern === undefined) return null; return { name: patternName, patternData: pattern.pattern, colorPalette: cosmetics.colorPalettes?.[colorPalette], } satisfies PlayerPattern; } setSelectedPatternName(patternName: string | undefined): void { if (patternName === undefined) { localStorage.removeItem(PATTERN_KEY); } else { localStorage.setItem(PATTERN_KEY, patternName); } this.emitChange("pattern", patternName); } getSelectedColor(): string | undefined { const data = localStorage.getItem("settings.territoryColor") ?? undefined; if (data === undefined) return undefined; return data; } setSelectedColor(color: string | undefined): void { if (color === undefined) { localStorage.removeItem("settings.territoryColor"); } else { localStorage.setItem("settings.territoryColor", color); } } getFlag(): string | null { let flag = localStorage.getItem("flag"); 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); } return flag; } setFlag(flag: string): void { if (flag === "country:xx") { this.clearFlag(); } else { localStorage.setItem("flag", flag); } console.log("emitting change!"); this.emitChange("flag", flag); } clearFlag(): void { localStorage.removeItem("flag"); } backgroundMusicVolume(): number { return this.getFloat("settings.backgroundMusicVolume", 0); } setBackgroundMusicVolume(volume: number): void { this.setFloat("settings.backgroundMusicVolume", volume); } attackRatioIncrement(): number { const increment = Math.round( this.getFloat("settings.attackRatioIncrement", 10), ); if (!Number.isFinite(increment) || increment <= 0) return 10; return increment; } soundEffectsVolume(): number { return this.getFloat("settings.soundEffectsVolume", 1); } setSoundEffectsVolume(volume: number): void { this.setFloat("settings.soundEffectsVolume", volume); } }