import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { formatKeyForDisplay, translateText } from "../client/Utils"; import { 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 "./FlagInputModal"; interface FlagInputModalElement extends HTMLElement { open(): void; returnTo?: string; } const isMac = typeof navigator !== "undefined" && /Mac/.test(navigator.userAgent); const DefaultKeybinds: Record = { toggleView: "Space", buildCity: "Digit1", buildFactory: "Digit2", buildPort: "Digit3", buildDefensePost: "Digit4", buildMissileSilo: "Digit5", buildSamLauncher: "Digit6", buildWarship: "Digit7", buildAtomBomb: "Digit8", buildHydrogenBomb: "Digit9", buildMIRV: "Digit0", attackRatioDown: "KeyT", attackRatioUp: "KeyY", boatAttack: "KeyB", groundAttack: "KeyG", swapDirection: "KeyU", zoomOut: "KeyQ", zoomIn: "KeyE", centerCamera: "KeyC", moveUp: "KeyW", moveLeft: "KeyA", moveDown: "KeyS", moveRight: "KeyD", modifierKey: isMac ? "MetaLeft" : "ControlLeft", altKey: "AltLeft", }; @customElement("user-setting") export class UserSettingModal extends BaseModal { private userSettings: UserSettings = new UserSettings(); @state() private activeTab: "basic" | "keybinds" = "basic"; @state() private keySequence: string[] = []; @state() private showEasterEggSettings = false; @state() private keybinds: Record< string, { value: string | string[]; key: string } > = {}; connectedCallback() { super.connectedCallback(); this.loadKeybindsFromStorage(); } disconnectedCallback() { window.removeEventListener("keydown", this.handleEasterEggKey); super.disconnectedCallback(); } private loadKeybindsFromStorage() { const savedKeybinds = localStorage.getItem("settings.keybinds"); if (!savedKeybinds) return; try { const parsed = JSON.parse(savedKeybinds); if ( typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ) { const isValid = Object.values(parsed).every((entry) => { if ( typeof entry !== "object" || entry === null || Array.isArray(entry) ) { return false; } if (!("key" in entry) || typeof (entry as any).key !== "string") { return false; } if (!("value" in entry)) { return false; } const value = (entry as any).value; if (typeof value === "string") { return true; } if (Array.isArray(value)) { return value.every((v) => typeof v === "string"); } return false; }); if (isValid) { this.keybinds = parsed; } else { console.warn( "Invalid keybinds structure: entries must be objects with 'key' (string) and 'value' (string or string[]) properties. Ignoring saved data.", ); } } else { console.warn( "Invalid keybinds data: expected non-array object. Ignoring saved data.", ); } } catch (e) { console.warn("Invalid keybinds JSON:", e); } } private handleKeybindChange( e: CustomEvent<{ action: string; value: string; key: string; prevValue?: string; }>, ) { const { action, value, key, prevValue } = e.detail; const activeKeybinds: Record = { ...DefaultKeybinds }; for (const [k, v] of Object.entries(this.keybinds)) { const normalizedValue = Array.isArray(v.value) ? v.value[0] || "" : 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}"]`, ) as SettingKeybind; if (element) { element.value = prevValue ?? DefaultKeybinds[action] ?? ""; element.requestUpdate(); } return; } this.keybinds = { ...this.keybinds, [action]: { value: value, key: key } }; localStorage.setItem("settings.keybinds", JSON.stringify(this.keybinds)); } private getKeyValue(action: string): string | undefined { const entry = this.keybinds[action]; if (!entry) return undefined; const normalizedValue = Array.isArray(entry.value) ? entry.value[0] || "" : entry.value; if (normalizedValue === "Null") return ""; return normalizedValue || undefined; } private getKeyChar(action: string): string { const entry = this.keybinds[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(e: CustomEvent<{ checked: boolean }>) { const enabled = e.detail?.checked; if (typeof enabled !== "boolean") { console.warn("Unexpected toggle event payload", e); return; } this.userSettings.set("settings.darkMode", enabled); if (enabled) { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } this.dispatchEvent( new CustomEvent("dark-mode-changed", { detail: { darkMode: enabled }, bubbles: true, composed: true, }), ); console.log("🌙 Dark Mode:", enabled ? "ON" : "OFF"); } private toggleEmojis(e: CustomEvent<{ checked: boolean }>) { const enabled = e.detail?.checked; if (typeof enabled !== "boolean") return; this.userSettings.set("settings.emojis", enabled); console.log("🤡 Emojis:", enabled ? "ON" : "OFF"); } private toggleAlertFrame(e: CustomEvent<{ checked: boolean }>) { const enabled = e.detail?.checked; if (typeof enabled !== "boolean") return; this.userSettings.set("settings.alertFrame", enabled); console.log("🚨 Alert frame:", enabled ? "ON" : "OFF"); } private toggleFxLayer(e: CustomEvent<{ checked: boolean }>) { const enabled = e.detail?.checked; if (typeof enabled !== "boolean") return; this.userSettings.set("settings.specialEffects", enabled); console.log("💥 Special effects:", enabled ? "ON" : "OFF"); } private toggleStructureSprites(e: CustomEvent<{ checked: boolean }>) { const enabled = e.detail?.checked; if (typeof enabled !== "boolean") return; this.userSettings.set("settings.structureSprites", enabled); console.log("🏠 Structure sprites:", enabled ? "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 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"); this.requestUpdate(); } private sliderAttackRatio(e: CustomEvent<{ value: number }>) { const value = e.detail?.value; if (typeof value === "number") { const ratio = value / 100; localStorage.setItem("settings.attackRatio", ratio.toString()); } else { console.warn("Slider event missing detail.value", e); } } private sliderTroopRatio(e: CustomEvent<{ value: number }>) { const value = e.detail?.value; if (typeof value === "number") { const ratio = value / 100; localStorage.setItem("settings.troopRatio", ratio.toString()); } 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.setFloat( "settings.attackRatioIncrement", Math.round(value), ); this.requestUpdate(); } private toggleTerritoryPatterns(e: CustomEvent<{ checked: boolean }>) { const enabled = e.detail?.checked; if (typeof enabled !== "boolean") return; this.userSettings.set("settings.territoryPatterns", enabled); console.log("🏳️ Territory Patterns:", enabled ? "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 openFlagSelector = () => { const flagInputModal = document.querySelector("#flag-input-modal"); if (flagInputModal?.open) { this.close(); flagInputModal.returnTo = "#" + (this.id || "page-settings"); flagInputModal.open(); } }; render() { const activeContent = this.activeTab === "basic" ? this.renderBasicSettings() : this.renderKeybindSettings(); const content = html` ${modalHeader({ title: translateText("user_setting.title"), onBack: () => this.close(), ariaLabel: translateText("common.back"), showDivider: true, })} (this.activeTab = "basic")} > ${translateText("user_setting.tab_basic")} (this.activeTab = "keybinds")} > ${translateText("user_setting.tab_keybinds")} ${activeContent} `; if (this.inline) { return content; } return html` ${content} `; } protected onClose(): void { window.removeEventListener("keydown", this.handleEasterEggKey); } private renderKeybindSettings() { return html` ${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.zoom_controls")} ${translateText("user_setting.camera_movement")} `; } private renderBasicSettings() { return html` { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); this.openFlagSelector(); } }} > ${translateText("flag_input.title")} ${translateText("flag_input.button_title")} ) => this.toggleDarkMode(e)} > ${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(); } public open() { super.open(); } }