diff --git a/resources/lang/en.json b/resources/lang/en.json index e3679dbe4..600f308f6 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -189,5 +189,45 @@ }, "select_lang": { "title": "Select Language" + }, + "user_setting": { + "title": "User Settings", + "tab_basic": "Basic Settings", + "tab_keybinds": "Keybinds", + "dark_mode_label": "🌙 Dark Mode", + "dark_mode_desc": "Toggle the site’s appearance between light and dark themes", + "emojis_label": "😊 Emojis", + "emojis_desc": "Toggle whether emojis are shown in game", + "left_click_label": "🖱️ Left Click to Open Menu", + "left_click_desc": "When ON, left-click opens menu and sword button attacks. When OFF, left-click attacks directly.", + "attack_ratio_label": "⚔️ Attack Ratio", + "attack_ratio_desc": "What percentage of your troops to send in an attack (1–100%)", + "troop_ratio_label": "🪖🛠️ Troops and Workers Ratio", + "troop_ratio_desc": "Adjust the balance between troops (for combat) and workers (for gold production) (1–100%)", + "easter_writing_speed_label": "Writing Speed Multiplier", + "easter_writing_speed_desc": "Adjust how fast you pretend to code (x1–x100)", + "easter_bug_count_label": "Bug Count", + "easter_bug_count_desc": "How many bugs you're okay with (0–1000, emotionally)", + "view_options": "View Options", + "toggle_view": "Toggle View", + "toggle_view_desc": "Alternate view (terrain/countries)", + "zoom_controls": "Zoom Controls", + "zoom_out": "Zoom Out", + "zoom_out_desc": "Zoom out the map", + "zoom_in": "Zoom In", + "zoom_in_desc": "Zoom in the map", + "camera_movement": "Camera Movement", + "center_camera": "Center Camera", + "center_camera_desc": "Center camera on player", + "move_up": "Move Camera Up", + "move_up_desc": "Move the camera upward", + "move_left": "Move Camera Left", + "move_left_desc": "Move the camera to the left", + "move_down": "Move Camera Down", + "move_down_desc": "Move the camera downward", + "move_right": "Move Camera Right", + "move_right_desc": "Move the camera to the right", + "reset": "Reset", + "unbind": "Unbind" } } diff --git a/resources/lang/es.json b/resources/lang/es.json index 811eca357..fee61b929 100644 --- a/resources/lang/es.json +++ b/resources/lang/es.json @@ -171,5 +171,45 @@ "game_mode": { "ffa": "Todos contra Todos", "teams": "Equipos" + }, + "user_setting": { + "title": "Uzantparametroj", + "tab_basic": "Bazaj parametroj", + "tab_keybinds": "Fulmoklavoj", + "dark_mode_label": "🌙 Malhela Modo", + "dark_mode_desc": "Baskuli la retpaĝa aspekto inter hela kaj malhela temo", + "emojis_label": "😊 Emoĝioj", + "emojis_desc": "Montri/Maski la emoĝiojn en la ludo", + "left_click_label": "🖱️Maldekstra alklako por malfermi menuon", + "left_click_desc": "Kiam aktiviga, maldekstra alklako malfermas menuon kaj glava atakbutono. Kiam malaktiviga, maldekstra alklako atakas direkten.", + "attack_ratio_label": "⚔️ Atakkvociento", + "attack_ratio_desc": "Kian procenton de viaj trupoj sendi en atako (1–100%)", + "troop_ratio_label": "🪖🛠️ Trupoj kaj Laboristoj kvociento", + "troop_ratio_desc": "Alĝustigu la ekvilibron inter soldatoj (por batalo) kaj laboristoj (por orproduktado) (1–100%)", + "easter_writing_speed_label": "Rapidskriba multiganto", + "easter_writing_speed_desc": "Alĝustigu kiom rapide vi ŝajnigas kodi (x1–x100)", + "easter_bug_count_label": "Nombro da cimoj", + "easter_bug_count_desc": "Kiom da cimoj vi bonfartas (0–1000, emocie)", + "view_options": "Vidi opciojn", + "toggle_view": "Baskuli vido", + "toggle_view_desc": "Alterna vido (tereno/landoj)", + "zoom_controls": "Zomaj kontroloj", + "zoom_out": "Malzomi", + "zoom_out_desc": "Malzomi la mapon", + "zoom_in": "Zomi", + "zoom_in_desc": "Zomi en la mapon", + "camera_movement": "Fotila movado", + "center_camera": "Centriĝi la fotilon", + "center_camera_desc": "Centriĝi la fotilon sur la ludanto", + "move_up": "Movi Fotilon Supren", + "move_up_desc": "Movi la fotilon supren", + "move_left": "Movi Fotilon Maldekstren", + "move_left_desc": "Movi la fotilon maldekstren", + "move_down": "Movi Fotilon Malsupren", + "move_down_desc": "Movi la fotilon malsupren", + "move_right": "Movi Fotilon Dekstren", + "move_right_desc": "Movi la fotilon dekstren", + "reset": "Rekomencigi", + "unbind": "Senligi" } } diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index ae495e9fe..f5dc49f46 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -113,6 +113,17 @@ export class InputHandler { ) {} initialize() { + const keybinds = { + toggleView: "Space", + centerCamera: "KeyC", + moveUp: "KeyW", + moveDown: "KeyS", + moveLeft: "KeyA", + moveRight: "KeyD", + zoomOut: "KeyQ", + zoomIn: "KeyE", + ...JSON.parse(localStorage.getItem("settings.keybinds") ?? "{}"), + }; this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e)); window.addEventListener("pointerup", (e) => this.onPointerUp(e)); this.canvas.addEventListener( @@ -122,59 +133,65 @@ export class InputHandler { this.onShiftScroll(e); e.preventDefault(); }, - { - passive: false, - }, + { passive: false }, ); window.addEventListener("pointermove", this.onPointerMove.bind(this)); - this.canvas.addEventListener("contextmenu", (e: MouseEvent) => { - this.onContextMenu(e); - }); + this.canvas.addEventListener("contextmenu", (e) => this.onContextMenu(e)); window.addEventListener("mousemove", (e) => { - if (e.movementX == 0 && e.movementY == 0) { - return; + if (e.movementX || e.movementY) { + this.eventBus.emit(new MouseMoveEvent(e.clientX, e.clientY)); } - this.eventBus.emit(new MouseMoveEvent(e.clientX, e.clientY)); }); this.pointers.clear(); - // Initialize the combined movement interval this.moveInterval = setInterval(() => { let deltaX = 0; let deltaY = 0; - // Handle both WASD and arrow keys - if (this.activeKeys.has("KeyW") || this.activeKeys.has("ArrowUp")) + if ( + this.activeKeys.has(keybinds.moveUp) || + this.activeKeys.has("ArrowUp") + ) deltaY += this.PAN_SPEED; - if (this.activeKeys.has("KeyS") || this.activeKeys.has("ArrowDown")) + if ( + this.activeKeys.has(keybinds.moveDown) || + this.activeKeys.has("ArrowDown") + ) deltaY -= this.PAN_SPEED; - if (this.activeKeys.has("KeyA") || this.activeKeys.has("ArrowLeft")) + if ( + this.activeKeys.has(keybinds.moveLeft) || + this.activeKeys.has("ArrowLeft") + ) deltaX += this.PAN_SPEED; - if (this.activeKeys.has("KeyD") || this.activeKeys.has("ArrowRight")) + if ( + this.activeKeys.has(keybinds.moveRight) || + this.activeKeys.has("ArrowRight") + ) deltaX -= this.PAN_SPEED; - if (deltaX !== 0 || deltaY !== 0) { + if (deltaX || deltaY) { this.eventBus.emit(new DragEvent(deltaX, deltaY)); } - // Handle zooming - const screenCenterX = window.innerWidth / 2; - const screenCenterY = window.innerHeight / 2; + const cx = window.innerWidth / 2; + const cy = window.innerHeight / 2; - if (this.activeKeys.has("Minus") || this.activeKeys.has("KeyQ")) { - this.eventBus.emit( - new ZoomEvent(screenCenterX, screenCenterY, this.ZOOM_SPEED), - ); + if ( + this.activeKeys.has(keybinds.zoomOut) || + this.activeKeys.has("Minus") + ) { + this.eventBus.emit(new ZoomEvent(cx, cy, this.ZOOM_SPEED)); } - if (this.activeKeys.has("Equal") || this.activeKeys.has("KeyE")) { - this.eventBus.emit( - new ZoomEvent(screenCenterX, screenCenterY, -this.ZOOM_SPEED), - ); + if ( + this.activeKeys.has(keybinds.zoomIn) || + this.activeKeys.has("Equal") + ) { + this.eventBus.emit(new ZoomEvent(cx, cy, -this.ZOOM_SPEED)); } }, 1); window.addEventListener("keydown", (e) => { - if (e.code === "Space") { + if (e.code === keybinds.toggleView) { e.preventDefault(); if (!this.alternateView) { this.alternateView = true; @@ -187,24 +204,23 @@ export class InputHandler { this.eventBus.emit(new CloseViewEvent()); } - // Add all movement keys to activeKeys if ( [ - "KeyW", - "KeyA", - "KeyS", - "KeyD", + keybinds.moveUp, + keybinds.moveDown, + keybinds.moveLeft, + keybinds.moveRight, + keybinds.zoomOut, + keybinds.zoomIn, "ArrowUp", "ArrowLeft", "ArrowDown", "ArrowRight", "Minus", "Equal", - "KeyE", - "KeyQ", "Digit1", "Digit2", - "KeyC", + keybinds.centerCamera, "ControlLeft", "ControlRight", ].includes(e.code) @@ -212,13 +228,13 @@ export class InputHandler { this.activeKeys.add(e.code); } }); - window.addEventListener("keyup", (e) => { - if (e.code === "Space") { + if (e.code === keybinds.toggleView) { e.preventDefault(); this.alternateView = false; this.eventBus.emit(new AlternateViewEvent(false)); } + if (e.key.toLowerCase() === "r" && e.altKey && !e.ctrlKey) { e.preventDefault(); this.eventBus.emit(new RefreshGraphicsEvent()); @@ -234,35 +250,12 @@ export class InputHandler { this.eventBus.emit(new AttackRatioEvent(10)); } - if (e.code === "KeyC") { + if (e.code === keybinds.centerCamera) { e.preventDefault(); this.eventBus.emit(new CenterCameraEvent()); } - // Remove all movement keys from activeKeys - if ( - [ - "KeyW", - "KeyA", - "KeyS", - "KeyD", - "ArrowUp", - "ArrowLeft", - "ArrowDown", - "ArrowRight", - "Minus", - "Equal", - "KeyE", - "KeyQ", - "Digit1", - "Digit2", - "KeyC", - "ControlLeft", - "ControlRight", - ].includes(e.code) - ) { - this.activeKeys.delete(e.code); - } + this.activeKeys.delete(e.code); }); } diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts index 8698e8483..366e5c298 100644 --- a/src/client/LangSelector.ts +++ b/src/client/LangSelector.ts @@ -173,6 +173,7 @@ export class LangSelector extends LitElement { "help-modal", "username-input", "public-lobby", + "user-setting", "o-modal", "o-button", ]; diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 8c065a1f7..eddba88b7 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -1,6 +1,9 @@ import { LitElement, html } from "lit"; import { customElement, query, state } from "lit/decorators.js"; +import { 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/SettingSlider"; import "./components/baseComponents/setting/SettingToggle"; @@ -9,7 +12,8 @@ import "./components/baseComponents/setting/SettingToggle"; export class UserSettingModal extends LitElement { private userSettings: UserSettings = new UserSettings(); - @state() private darkMode: boolean = this.userSettings.darkMode(); + @state() private settingsMode: "basic" | "keybinds" = "basic"; + @state() private keybinds: Record = {}; @state() private keySequence: string[] = []; @state() private showEasterEggSettings = false; @@ -17,6 +21,15 @@ export class UserSettingModal extends LitElement { connectedCallback() { super.connectedCallback(); window.addEventListener("keydown", this.handleKeyDown); + + const savedKeybinds = localStorage.getItem("settings.keybinds"); + if (savedKeybinds) { + try { + this.keybinds = JSON.parse(savedKeybinds); + } catch (e) { + console.warn("Invalid keybinds JSON:", e); + } + } } @query("o-modal") private modalEl!: HTMLElement & { @@ -119,96 +132,63 @@ export class UserSettingModal extends LitElement { } } + private handleKeybindChange( + e: CustomEvent<{ action: string; value: string }>, + ) { + const { action, value } = e.detail; + const prevValue = this.keybinds[action] ?? ""; + + const values = Object.entries(this.keybinds) + .filter(([k]) => k !== action) + .map(([, v]) => v); + if (values.includes(value) && value !== "Null") { + const popup = document.createElement("div"); + popup.className = "setting-popup"; + popup.textContent = `The key "${value}" is already assigned to another action.`; + document.body.appendChild(popup); + const element = this.renderRoot.querySelector( + `setting-keybind[action="${action}"]`, + ) as SettingKeybind; + if (element) { + element.value = prevValue; + element.requestUpdate(); + } + return; + } + this.keybinds = { ...this.keybinds, [action]: value }; + localStorage.setItem("settings.keybinds", JSON.stringify(this.keybinds)); + } + render() { return html` - + @@ -216,7 +196,194 @@ export class UserSettingModal extends LitElement { `; } + private renderBasicSettings() { + return html` + + ) => + this.toggleDarkMode(e)} + > + + + + + + + + + + + + + + ${this.showEasterEggSettings + ? html` + { + const value = e.detail?.value; + if (typeof value !== "undefined") { + console.log("Changed:", value); + } else { + console.warn("Slider event missing detail.value", e); + } + }} + > + + { + const value = e.detail?.value; + if (typeof value !== "undefined") { + console.log("Changed:", value); + } else { + console.warn("Slider event missing detail.value", e); + } + }} + > + ` + : null} + `; + } + + private renderKeybindSettings() { + return html` +
+ ${translateText("user_setting.view_options")} +
+ + + +
+ ${translateText("user_setting.zoom_controls")} +
+ + + + + +
+ ${translateText("user_setting.camera_movement")} +
+ + + + + + + + + + + `; + } + public open() { + this.requestUpdate(); this.modalEl?.open(); } diff --git a/src/client/components/baseComponents/setting/SettingKeybind.ts b/src/client/components/baseComponents/setting/SettingKeybind.ts new file mode 100644 index 000000000..049306613 --- /dev/null +++ b/src/client/components/baseComponents/setting/SettingKeybind.ts @@ -0,0 +1,115 @@ +import { LitElement, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { translateText } from "../../../../client/Utils"; + +@customElement("setting-keybind") +export class SettingKeybind extends LitElement { + @property() label = "Setting"; + @property() description = ""; + @property({ type: String, reflect: true }) action = ""; + @property({ type: String }) defaultKey = ""; + @property({ type: String }) value = ""; + @property({ type: Boolean }) easter = false; + + createRenderRoot() { + return this; + } + + private listening = false; + + render() { + return html` +
+
+ + +
+
${this.description}
+ +
+ + ${this.displayKey(this.value || this.defaultKey)} + + + + +
+
+
+
+ `; + } + + private displayKey(key: string): string { + if (key === " ") return "Space"; + if (key.startsWith("Key") && key.length === 4) { + return key.slice(3); + } + return key.length + ? key.charAt(0).toUpperCase() + key.slice(1) + : "Press a key"; + } + + private startListening() { + this.listening = true; + this.requestUpdate(); + } + + private handleKeydown(e: KeyboardEvent) { + if (!this.listening) return; + e.preventDefault(); + + const code = e.code; + + this.value = code; + + this.dispatchEvent( + new CustomEvent("change", { + detail: { action: this.action, value: code }, + bubbles: true, + composed: true, + }), + ); + + this.listening = false; + this.requestUpdate(); + } + + private resetToDefault() { + this.value = this.defaultKey; + this.dispatchEvent( + new CustomEvent("change", { + detail: { action: this.action, value: this.defaultKey }, + bubbles: true, + composed: true, + }), + ); + } + + private unbindKey() { + this.value = ""; + this.dispatchEvent( + new CustomEvent("change", { + detail: { action: this.action, value: "Null" }, + bubbles: true, + composed: true, + }), + ); + this.requestUpdate(); + } +} diff --git a/src/client/styles/components/controls.css b/src/client/styles/components/controls.css index 0bd8d162f..4acd0d4b3 100644 --- a/src/client/styles/components/controls.css +++ b/src/client/styles/components/controls.css @@ -78,3 +78,14 @@ font-size: 14px; color: #ccc; } + +.setting-input.keybind:hover .key, +.setting-input.keybind:focus .key { + background-color: #333; + box-shadow: 0 2px 0 #222; +} + +.setting-input.keybind.listening .key { + background-color: #1d4ed8; /* blue-700 */ + box-shadow: 0 2px 0 #0f172a; /* darker blue */ +} diff --git a/src/client/styles/components/setting.css b/src/client/styles/components/setting.css index 051b80f3c..9a71be176 100644 --- a/src/client/styles/components/setting.css +++ b/src/client/styles/components/setting.css @@ -22,6 +22,10 @@ gap: 12px; } +.setting-item.column { + flex-direction: column; +} + @keyframes rainbow-background { 0% { background-position: 0% 50%; @@ -64,6 +68,20 @@ z-index: 9999; } +.setting-popup { + position: fixed; + top: 40px; + left: 50%; + transform: translate(-50%, -50%) scale(0.9); + padding: 16px 24px; + background: rgba(0, 0, 0, 0.8); + color: #fff; + font-size: 20px; + border-radius: 12px; + animation: fadePop_2 10s ease-out forwards; + z-index: 9999; +} + @keyframes fadePop { 0% { opacity: 0; @@ -82,6 +100,25 @@ } } +@keyframes fadePop_2 { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.6); + } + 5% { + opacity: 1; + transform: translate(-50%, -50%) scale(1.05); + } + 95% { + opacity: 1; + transform: translate(-50%, -50%) scale(1.05); + } + 100% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.9); + } +} + .setting-item:hover { background: #2a2a2a; } @@ -158,17 +195,14 @@ cursor: pointer; } -.setting-input.slider::-moz-range-thumb { - width: 18px; - height: 18px; - border-radius: 50%; - background: #fff; - border: 2px solid #2196f3; - cursor: pointer; +.setting-input.slider::-moz-range-track { + background-color: #444; + height: 10px; + border-radius: 5px; } -.setting-input.slider::-moz-range-track { - background: linear-gradient(to right, #2196f3 50%, #444 50%); +.setting-input.slider::-moz-range-progress { + background-color: #2196f3; height: 10px; border-radius: 5px; } @@ -255,3 +289,38 @@ white-space: normal; word-break: break-word; } + +.setting-keybind-box { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.setting-keybind-description { + flex: 1; + font-size: 0.75rem; + color: #e5e5e5; + word-break: break-word; + overflow-wrap: break-word; + min-width: 0; +} + +.setting-key { + background-color: black; + color: white; + font-weight: 600; + padding: 4px 12px; + border-radius: 6px; + font-family: monospace; + font-size: 0.875rem; + box-shadow: 0 2px 0 #444; + white-space: nowrap; + user-select: none; + outline: none; +} + +.setting-key:focus { + outline: 2px solid #3b82f6; + outline-offset: 2px; +}