import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { formatKeyForDisplay, translateText } from "../client/Utils"; import { getDefaultKeybinds, UserSettings } from "../core/game/UserSettings"; import "./components/baseComponents/setting/SettingKeybind"; import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind"; import "./components/baseComponents/setting/SettingNumber"; import "./components/baseComponents/setting/SettingSelect"; import "./components/baseComponents/setting/SettingSlider"; import "./components/baseComponents/setting/SettingToggle"; import { BaseModal } from "./components/BaseModal"; import { modalHeader } from "./components/ui/ModalHeader"; import { Platform } from "./Platform"; @customElement("user-setting") export class UserSettingModal extends BaseModal { protected routerName = "settings"; private userSettings: UserSettings = new UserSettings(); private readonly defaultKeybinds = getDefaultKeybinds(Platform.isMac); @state() private keySequence: string[] = []; @state() private showEasterEggSettings = false; @state() private userKeybinds: Record< string, { value: string; key: string } > = {}; connectedCallback() { super.connectedCallback(); this.loadKeybindsFromStorage(); } disconnectedCallback() { window.removeEventListener("keydown", this.handleEasterEggKey); super.disconnectedCallback(); } private loadKeybindsFromStorage() { const parsed = this.userSettings.parsedUserKeybinds(); if (Object.keys(parsed).length === 0) { this.userKeybinds = {}; return; } const validated: Record = {}; for (const [action, entry] of Object.entries(parsed)) { if (typeof entry === "string") { validated[action] = { value: entry, key: entry }; } else if ( typeof entry === "object" && entry !== null && !Array.isArray(entry) ) { const rawValue = (entry as any).value ?? "Null"; const value = Array.isArray(rawValue) ? rawValue.find((v) => typeof v === "string") : rawValue; const rawKey = (entry as any).key ?? value; const key = Array.isArray(rawKey) ? rawKey.find((v) => typeof v === "string") : rawKey; if (typeof value === "string" && typeof key === "string") { validated[action] = { value, key }; } } } this.userKeybinds = validated; } private handleKeybindChange( e: CustomEvent<{ action: string; value: string; key: string; prevValue?: string; }>, ) { const { action, value, key, prevValue } = e.detail; const activeKeybinds = { ...this.defaultKeybinds }; for (const [k, v] of Object.entries(this.userKeybinds)) { const normalizedValue = v.value; if (normalizedValue === "Null") { delete activeKeybinds[k]; } else { activeKeybinds[k] = normalizedValue; } } const values = Object.entries(activeKeybinds) .filter(([k]) => k !== action) .map(([, v]) => v); if (values.includes(value) && value !== "Null") { const displayKey = formatKeyForDisplay(key || value); window.dispatchEvent( new CustomEvent("show-message", { detail: { message: html` ${(() => { const message = translateText( "user_setting.keybind_conflict_error", { key: displayKey }, ); const parts = message.split(displayKey); return html`${parts[0]}${displayKey}${parts[1] || ""}`; })()} `, color: "red", duration: 3000, }, }), ); const element = this.renderRoot.querySelector( `setting-keybind[action="${action}"]`, ); if (element) { element.value = prevValue ?? this.defaultKeybinds[action] ?? ""; element.requestUpdate(); } return; } this.userKeybinds = { ...this.userKeybinds, [action]: { value: value, key: key }, }; this.userSettings.setKeybinds(this.userKeybinds); } private getKeyValue(action: string): string | undefined { const entry = this.userKeybinds[action]; if (!entry) return undefined; const normalizedValue = entry.value; if (normalizedValue === "Null") return ""; return normalizedValue || undefined; } private getKeyChar(action: string): string { const entry = this.userKeybinds[action]; if (!entry) return ""; return entry.key || ""; } private handleEasterEggKey = (e: KeyboardEvent) => { if (!this.isModalOpen || this.showEasterEggSettings) return; // Validate that the event target is inside this component const target = e.target as Node; if (!this.contains(target)) { return; } const key = e.key.toLowerCase(); const nextSequence = [...this.keySequence, key].slice(-4); this.keySequence = nextSequence; if (nextSequence.join("") === "evan") { this.triggerEasterEgg(); this.keySequence = []; } }; private triggerEasterEgg() { console.log("🪺 Setting~ unlocked by EVAN combo!"); this.showEasterEggSettings = true; const popup = document.createElement("div"); popup.className = "fixed top-10 left-1/2 p-4 px-6 bg-black/80 text-white text-xl rounded-xl animate-fadePop z-[9999]"; popup.textContent = "🎉 You found a secret setting!"; document.body.appendChild(popup); setTimeout(() => { popup.remove(); }, 5000); } toggleDarkMode() { this.userSettings.toggleDarkMode(); console.log("🌙 Dark Mode:", this.userSettings.darkMode() ? "ON" : "OFF"); } /** Whether colorblind mode is currently enabled in the graphics overrides. */ private colorblindMode(): boolean { return ( this.userSettings.graphicsOverrides().accessibility?.colorblind ?? false ); } /** Flip the colorblind-mode graphics override and persist it. */ private toggleColorblindMode() { const overrides = this.userSettings.graphicsOverrides(); this.userSettings.setGraphicsOverrides({ ...overrides, accessibility: { ...overrides.accessibility, colorblind: !this.colorblindMode(), }, }); } private toggleEmojis() { this.userSettings.toggleEmojis(); console.log("🤡 Emojis:", this.userSettings.emojis() ? "ON" : "OFF"); } private toggleAlertFrame() { this.userSettings.toggleAlertFrame(); console.log( "🚨 Alert frame:", this.userSettings.alertFrame() ? "ON" : "OFF", ); } private toggleCursorCostLabel() { this.userSettings.toggleCursorCostLabel(); console.log( "💰 Cursor build cost:", this.userSettings.cursorCostLabel() ? "ON" : "OFF", ); } private toggleAnonymousNames() { this.userSettings.toggleRandomName(); console.log( "🙈 Anonymous Names:", this.userSettings.anonymousNames() ? "ON" : "OFF", ); } private toggleLobbyIdVisibility() { this.userSettings.toggleLobbyIdVisibility(); console.log( "👁️ Hidden Lobby IDs:", !this.userSettings.lobbyIdVisibility() ? "ON" : "OFF", ); } private toggleLeftClickOpensMenu() { this.userSettings.toggleLeftClickOpenMenu(); console.log( "🖱️ Left Click Opens Menu:", this.userSettings.leftClickOpensMenu() ? "ON" : "OFF", ); this.requestUpdate(); } private sliderAttackRatio(e: CustomEvent<{ value: number }>) { const value = e.detail?.value; if (typeof value === "number") { const ratio = value / 100; this.userSettings.setAttackRatio(ratio); } else { console.warn("Slider event missing detail.value", e); } } private changeAttackRatioIncrement( e: CustomEvent<{ value: number | string }>, ) { const rawValue = e.detail?.value; const value = typeof rawValue === "number" ? rawValue : parseInt(String(rawValue), 10); if (!Number.isFinite(value)) { console.warn("Select event missing detail.value", e); return; } this.userSettings.setAttackRatioIncrement(Math.round(value)); this.requestUpdate(); } private toggleTerritoryPatterns() { this.userSettings.toggleTerritoryPatterns(); console.log( "🏳️ Territory Patterns:", this.userSettings.territoryPatterns() ? "ON" : "OFF", ); } private toggleGoToPlayer() { this.userSettings.toggleGoToPlayer(); console.log( "🔍 Go to player:", this.userSettings.goToPlayer() ? "ON" : "OFF", ); } private togglePerformanceOverlay() { this.userSettings.togglePerformanceOverlay(); } protected modalConfig() { return { tabs: [ { key: "basic", label: translateText("user_setting.tab_basic") }, { key: "keybinds", label: translateText("user_setting.tab_keybinds") }, ], }; } protected renderHeaderSlot() { return modalHeader({ title: translateText("user_setting.title"), onBack: () => this.close(), ariaLabel: translateText("common.back"), showDivider: true, }); } protected renderBody(tab: string) { const body = tab === "keybinds" ? this.renderKeybindSettings() : this.renderBasicSettings(); return html`
${body}
`; } protected onClose(): void { window.removeEventListener("keydown", this.handleEasterEggKey); } private renderKeybindSettings() { return html`
${translateText("user_setting.keybinds_hint")}

${translateText("user_setting.view_options")}

${translateText("user_setting.build_controls")}

${translateText("user_setting.menu_shortcuts")}

${translateText("user_setting.attack_ratio_controls")}

${translateText("user_setting.attack_keybinds")}

${translateText("user_setting.ally_keybinds")}

${translateText("user_setting.zoom_controls")}

${translateText("user_setting.camera_movement")}

`; } private renderBasicSettings() { return html` ${this.showEasterEggSettings ? html` { const value = e.detail?.value; if (value !== undefined) { console.log("Changed:", value); } else { console.warn("Slider event missing detail.value", e); } }} > { const value = e.detail?.value; if (value !== undefined) { console.log("Changed:", value); } else { console.warn("Slider event missing detail.value", e); } }} > ` : null} `; } protected onOpen(): void { window.addEventListener("keydown", this.handleEasterEggKey); this.loadKeybindsFromStorage(); } }