import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { formatKeyForDisplay, translateText } from "../client/Utils"; import { getDefaultKeybinds, KeybindAction, KeyUnbound, 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: Partial< Record > = {}; 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: Partial< Record > = {}; for (const [rawAction, entry] of Object.entries(parsed)) { const action = rawAction as KeybindAction; if (typeof entry === "string") { validated[action] = { value: entry, key: entry }; } else if ( typeof entry === "object" && entry !== null && !Array.isArray(entry) ) { const rawValue = entry.value ?? KeyUnbound; const value = Array.isArray(rawValue) ? rawValue.find((v) => typeof v === "string") : rawValue; const rawKey = entry.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: KeybindAction; value: string; key: string; prevValue?: string; }>, ) { const { action, value, prevValue } = e.detail; let { key } = e.detail; // TODO: remove after testing console.info( "handleKeybindChange received value: " + value, ", key: " + key, ); // Don't display "Dead" for Quote / Backquote https://en.wikipedia.org/wiki/QWERTY#US-International // nor "Unidentified" for some keys in Firefox ("" in Chrome). Empty the key to use value (key code). key = key === "Dead" || key === "Unidentified" ? "" : key; const activeKeybinds: Record = { ...this.defaultKeybinds, }; for (const [rawAction, codeAndKey] of Object.entries(this.userKeybinds)) { const action = rawAction as KeybindAction; const normalizedCode = codeAndKey.value; if (normalizedCode === KeyUnbound) { delete activeKeybinds[action]; } else { activeKeybinds[action] = normalizedCode; } } const codes = Object.entries(activeKeybinds) .filter(([a]) => a !== action) .map(([, code]) => code); if (codes.includes(value) && value !== KeyUnbound) { 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] ?? ""; // requestUpdate() handled by SettingKeyBind which dispatches the "change" event that triggers this function } return; } this.userKeybinds = { ...this.userKeybinds, [action]: { value: value, key: key }, }; this.userSettings.setUserKeybinds(this.userKeybinds); } private getKeyValue(action: KeybindAction): string | undefined { const entry = this.userKeybinds[action]; if (!entry) return undefined; const normalizedValue = entry.value; if (normalizedValue === KeyUnbound) return ""; return normalizedValue || undefined; } private getKeyChar(action: KeybindAction): 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"); } 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 toggleFxLayer() { this.userSettings.toggleFxLayer(); console.log( "💥 Special effects:", this.userSettings.fxLayer() ? "ON" : "OFF", ); } private toggleStructureSprites() { this.userSettings.toggleStructureSprites(); console.log( "🏠 Structure sprites:", this.userSettings.structureSprites() ? "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(); } }