From 7b853a8982db94a4da44b6fb7e7f632acee6e7b2 Mon Sep 17 00:00:00 2001 From: Sardi Date: Mon, 18 May 2026 16:00:37 +0200 Subject: [PATCH 1/3] Fix: Missing binds in settings --- resources/lang/en.json | 6 ++++ src/client/InputHandler.ts | 31 +++++++++++++------ src/client/UserSettingModal.ts | 31 ++++++++++++++++++- src/client/Utils.ts | 5 +++ .../baseComponents/setting/SettingKeybind.ts | 14 +++++++-- src/core/game/UserSettings.ts | 2 +- 6 files changed, 75 insertions(+), 14 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 8a57a4cf2d..fc213afbc0 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -741,6 +741,8 @@ "view_options": "View Options", "toggle_view": "Toggle View", "toggle_view_desc": "Alternate view (terrain/countries)", + "reset_gfx": "Reset Graphics", + "reset_gfx_desc": "Reset / refresh the graphics renderer.", "build_controls": "Build Controls", "build_city": "Build City", "build_city_desc": "Build a City under your cursor.", @@ -767,6 +769,8 @@ "build_menu_modifier_desc": "Hold this key while clicking to open the build menu.", "emoji_menu_modifier": "Emoji Menu Modifier", "emoji_menu_modifier_desc": "Hold this key while clicking to open the emoji menu.", + "shift_key": "Warship Box Select", + "shift_key_desc": "Hold this key and drag to draw a selection box that selects all warships within it.", "pause_game": "Pause", "pause_game_desc": "Pause or resume the game (single player and custom games for host).", "game_speed_up": "Game Speed Up", @@ -794,6 +798,8 @@ "break_alliance_desc": "Break alliance with the player whose tile is under your cursor.", "swap_direction": "Swap Rocket Direction", "swap_direction_desc": "Toggle rocket launch direction (up/down).", + "select_all_warships": "Select All Warships", + "select_all_warships_desc": "Select all your warships at once.", "zoom_controls": "Zoom Controls", "zoom_out": "Zoom Out", "zoom_out_desc": "Zoom out the map", diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index d2d2de2a21..1138c3502e 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -482,8 +482,7 @@ export class InputHandler { this.eventBus.emit(new AlternateViewEvent(false)); } - const resetKey = this.keybinds.resetGfx ?? "KeyR"; - if (e.code === resetKey && this.isAltKeyHeld(e)) { + if (this.keybindMatchesEvent(e, this.keybinds.resetGfx ?? "Alt+KeyR")) { e.preventDefault(); this.eventBus.emit(new RefreshGraphicsEvent()); } @@ -854,24 +853,36 @@ export class InputHandler { } /** - * Parses a keybind value that may include a "Shift+" prefix. - * e.g. "Shift+KeyB" → { shift: true, code: "KeyB" } - * "KeyB" → { shift: false, code: "KeyB" } + * Parses a keybind value that may include a "Shift+" or "Alt+" prefix. + * e.g. "Shift+KeyB" → { shift: true, alt: false, code: "KeyB" } + * "Alt+KeyR" → { shift: false, alt: true, code: "KeyR" } + * "KeyB" → { shift: false, alt: false, code: "KeyB" } */ - private parseKeybind(value: string): { shift: boolean; code: string } { + private parseKeybind(value: string): { + shift: boolean; + alt: boolean; + code: string; + } { if (value?.startsWith("Shift+")) { - return { shift: true, code: value.slice(6) }; + return { shift: true, alt: false, code: value.slice(6) }; } - return { shift: false, code: value }; + if (value?.startsWith("Alt+")) { + return { shift: false, alt: true, code: value.slice(4) }; + } + return { shift: false, alt: false, code: value }; } /** * Returns true if the keyboard event matches the given keybind value, - * including optional Shift+ prefix support. + * including optional Shift+ and Alt+ prefix support. */ private keybindMatchesEvent(e: KeyboardEvent, keybindValue: string): boolean { const parsed = this.parseKeybind(keybindValue); - return e.code === parsed.code && e.shiftKey === parsed.shift; + return ( + e.code === parsed.code && + e.shiftKey === parsed.shift && + e.altKey === parsed.alt + ); } /** diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index e36444ee79..8d38a0af85 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -91,7 +91,6 @@ export class UserSettingModal extends BaseModal { activeKeybinds[k] = normalizedValue; } } - const values = Object.entries(activeKeybinds) .filter(([k]) => k !== action) .map(([, v]) => v); @@ -401,6 +400,16 @@ export class UserSettingModal extends BaseModal { @change=${this.handleKeybindChange} > + +

@@ -639,6 +648,26 @@ export class UserSettingModal extends BaseModal { @change=${this.handleKeybindChange} > + + + +

diff --git a/src/client/Utils.ts b/src/client/Utils.ts index 943e764a4a..33b9e9399a 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -319,6 +319,11 @@ export function formatKeyForDisplay(value: string): string { return "Shift+" + formatKeyForDisplay(value.slice(6)); } + // Handle Alt+ prefix: format as "Alt+X" + if (value.startsWith("Alt+")) { + return "Alt+" + formatKeyForDisplay(value.slice(4)); + } + // Handle space character or "Space" key if (value === " " || value === "Space") return "Space"; diff --git a/src/client/components/baseComponents/setting/SettingKeybind.ts b/src/client/components/baseComponents/setting/SettingKeybind.ts index 9725db81ff..0c35a06bec 100644 --- a/src/client/components/baseComponents/setting/SettingKeybind.ts +++ b/src/client/components/baseComponents/setting/SettingKeybind.ts @@ -117,8 +117,18 @@ export class SettingKeybind extends LitElement { // Prevent default only for keys we're actually capturing e.preventDefault(); - const code = e.shiftKey ? `Shift+${e.code}` : e.code; - const displayKey = e.shiftKey ? `Shift+${e.key.toUpperCase()}` : e.key; + let code: string; + let displayKey: string; + if (e.shiftKey) { + code = `Shift+${e.code}`; + displayKey = `Shift+${e.key.toUpperCase()}`; + } else if (e.altKey) { + code = `Alt+${e.code}`; + displayKey = code; // e.key would give special chars (e.g. ® for Alt+R); use code instead + } else { + code = e.code; + displayKey = e.key; + } const prevValue = this.value; // Temporarily set the value to the new code for validation in parent diff --git a/src/core/game/UserSettings.ts b/src/core/game/UserSettings.ts index 9faff1a0c6..5e827d1d78 100644 --- a/src/core/game/UserSettings.ts +++ b/src/core/game/UserSettings.ts @@ -33,7 +33,7 @@ export function getDefaultKeybinds(isMac: boolean): Record { modifierKey: isMac ? "MetaLeft" : "ControlLeft", altKey: "AltLeft", shiftKey: "ShiftLeft", - resetGfx: "KeyR", + resetGfx: "Alt+KeyR", selectAllWarships: "KeyF", pauseGame: "KeyP", gameSpeedUp: "Period", From f2949fb53190c811183b988120094a2cfa8bfa87 Mon Sep 17 00:00:00 2001 From: Sardi Date: Mon, 18 May 2026 16:54:17 +0200 Subject: [PATCH 2/3] Fix: Missing binds in settings --- src/client/InputHandler.ts | 111 ++++++++++++++++-- src/client/Utils.ts | 5 + .../baseComponents/setting/SettingKeybind.ts | 47 +++++++- 3 files changed, 150 insertions(+), 13 deletions(-) diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 1138c3502e..c2b1556659 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -214,6 +214,8 @@ export class InputHandler { private selectionBoxActive: boolean = false; // True while warships are selected via box (waiting for move target click) private multiSelectionActive: boolean = false; + // True while the configured shiftKey (box-select hold key) is held down + private shiftKeybindHeld: boolean = false; // Touch long-press state private longPressTimer: ReturnType | null = null; @@ -311,8 +313,8 @@ export class InputHandler { let deltaX = 0; let deltaY = 0; - // Skip if shift is held down - if (this.activeKeys.has(this.keybinds.shiftKey)) { + // Skip if box-select key is held down + if (this.shiftKeybindHeld) { return; } @@ -437,7 +439,6 @@ export class InputHandler { this.keybinds.centerCamera, "ControlLeft", "ControlRight", - this.keybinds.shiftKey, ].includes(e.code) ) { this.activeKeys.add(e.code); @@ -445,7 +446,8 @@ export class InputHandler { // Shift = warship box selection mode. // If a ghost structure is active, discard it first. - if (e.code === this.keybinds.shiftKey) { + if (this.keybindMatchesEvent(e, this.keybinds.shiftKey)) { + this.shiftKeybindHeld = true; if (this.uiState.ghostStructure !== null) { this.setGhostStructure(null); } @@ -574,13 +576,13 @@ export class InputHandler { this.activeKeys.delete(e.code); - // Reset crosshair when Shift is released (unless selection box or multi-selection still active) - if ( - e.code === this.keybinds.shiftKey && - !this.selectionBoxActive && - !this.multiSelectionActive - ) { - this.canvas.style.cursor = ""; + // Reset crosshair when box-select key is released (unless selection box or multi-selection still active) + const parsedShiftKey = this.parseKeybind(this.keybinds.shiftKey); + if (e.code === parsedShiftKey.code) { + this.shiftKeybindHeld = false; + if (!this.selectionBoxActive && !this.multiSelectionActive) { + this.canvas.style.cursor = ""; + } } }); } @@ -691,6 +693,11 @@ export class InputHandler { } } + if (this.dispatchPointerKeybindActions(event)) { + this.suppressNextTap = false; + return; + } + if (this.isModifierKeyPressed(event)) { this.suppressNextTap = false; this.eventBus.emit(new ShowBuildMenuEvent(event.clientX, event.clientY)); @@ -803,7 +810,7 @@ export class InputHandler { // started, continue emitting selection box updates if ( this.selectionBoxActive || - this.activeKeys.has(this.keybinds.shiftKey) || + this.shiftKeybindHeld || this.longPressActive ) { this.selectionBoxActive = true; @@ -885,6 +892,86 @@ export class InputHandler { ); } + private static readonly MOUSE_BUTTON_NAMES: Record = { + 0: "MouseLeft", + 1: "MouseMiddle", + 2: "MouseRight", + }; + + private keybindMatchesPointerEvent( + e: PointerEvent, + keybindValue: string, + ): boolean { + if (!keybindValue) return false; + const parsed = this.parseKeybind(keybindValue); + const buttonName = InputHandler.MOUSE_BUTTON_NAMES[e.button]; + return ( + parsed.code === buttonName && + e.shiftKey === parsed.shift && + e.altKey === parsed.alt + ); + } + + private dispatchPointerKeybindActions(event: PointerEvent): boolean { + const actions: Array<[string, () => void]> = [ + [ + this.keybinds.boatAttack, + () => this.eventBus.emit(new DoBoatAttackEvent()), + ], + [ + this.keybinds.groundAttack, + () => this.eventBus.emit(new DoGroundAttackEvent()), + ], + [ + this.keybinds.retaliateAttack, + () => this.eventBus.emit(new DoRetaliateAttackEvent()), + ], + [ + this.keybinds.requestAlliance, + () => this.eventBus.emit(new DoRequestAllianceEvent()), + ], + [ + this.keybinds.breakAlliance, + () => this.eventBus.emit(new DoBreakAllianceEvent()), + ], + [ + this.keybinds.swapDirection, + () => this.eventBus.emit(new SwapRocketDirectionEvent(true)), + ], + [ + this.keybinds.selectAllWarships, + () => this.eventBus.emit(new SelectAllWarshipsEvent()), + ], + [ + this.keybinds.centerCamera, + () => this.eventBus.emit(new CenterCameraEvent()), + ], + [ + this.keybinds.pauseGame, + () => this.eventBus.emit(new TogglePauseIntentEvent()), + ], + [ + this.keybinds.gameSpeedUp, + () => this.eventBus.emit(new GameSpeedUpIntentEvent()), + ], + [ + this.keybinds.gameSpeedDown, + () => this.eventBus.emit(new GameSpeedDownIntentEvent()), + ], + [ + this.keybinds.resetGfx ?? "Alt+KeyR", + () => this.eventBus.emit(new RefreshGraphicsEvent()), + ], + ]; + for (const [keybind, action] of actions) { + if (this.keybindMatchesPointerEvent(event, keybind)) { + action(); + return true; + } + } + return false; + } + /** * Extracts the digit character from KeyboardEvent.code. * Codes look like "Digit0".."Digit9" (6 chars, digit at index 5) and diff --git a/src/client/Utils.ts b/src/client/Utils.ts index 33b9e9399a..76e25ec262 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -324,6 +324,11 @@ export function formatKeyForDisplay(value: string): string { return "Alt+" + formatKeyForDisplay(value.slice(4)); } + // Handle mouse buttons + if (value === "MouseLeft") return "Left"; + if (value === "MouseRight") return "Right"; + if (value === "MouseMiddle") return "Middle"; + // Handle space character or "Space" key if (value === " " || value === "Space") return "Space"; diff --git a/src/client/components/baseComponents/setting/SettingKeybind.ts b/src/client/components/baseComponents/setting/SettingKeybind.ts index 0c35a06bec..e22908525a 100644 --- a/src/client/components/baseComponents/setting/SettingKeybind.ts +++ b/src/client/components/baseComponents/setting/SettingKeybind.ts @@ -17,6 +17,7 @@ export class SettingKeybind extends LitElement { } private listening = false; + private suppressNextContextMenu = false; render() { const currentValue = this.value === "" ? "" : this.value || this.defaultKey; @@ -49,7 +50,8 @@ export class SettingKeybind extends LitElement { aria-label="${translateText("user_setting.press_a_key")}" tabindex="0" @keydown=${this.handleKeydown} - @click=${this.startListening} + @mousedown=${this.handleMousedown} + @contextmenu=${this.handleContextMenu} @blur=${this.handleBlur} > ${this.listening ? "..." : this.displayKey(displayValue)} @@ -147,6 +149,49 @@ export class SettingKeybind extends LitElement { this.requestUpdate(); } + private handleMousedown(e: MouseEvent) { + if (!this.listening) { + this.startListening(); + return; + } + if (!e.shiftKey && !e.altKey) return; // require a keyboard modifier + + const buttonMap: Record = { + 0: "MouseLeft", + 1: "MouseMiddle", + 2: "MouseRight", + }; + const buttonName = buttonMap[e.button]; + if (!buttonName) return; + + if (e.button === 2) this.suppressNextContextMenu = true; + + e.preventDefault(); + e.stopPropagation(); + + const prefix = e.shiftKey ? "Shift+" : "Alt+"; + const code = `${prefix}${buttonName}`; + const prevValue = this.value; + + this.value = code; + this.dispatchEvent( + new CustomEvent("change", { + detail: { action: this.action, value: code, key: code, prevValue }, + bubbles: true, + composed: true, + }), + ); + this.listening = false; + this.requestUpdate(); + } + + private handleContextMenu(e: MouseEvent) { + if (this.listening || this.suppressNextContextMenu) { + e.preventDefault(); + this.suppressNextContextMenu = false; + } + } + private handleBlur() { this.listening = false; this.requestUpdate(); From de4458e02b06e70b697fa812280892efb30f1458 Mon Sep 17 00:00:00 2001 From: Sardi Date: Mon, 18 May 2026 17:05:21 +0200 Subject: [PATCH 3/3] Solved requested changes by codrabbit --- src/client/InputHandler.ts | 29 ++++++++++++++++---- tests/InputHandler.test.ts | 6 ++-- tests/core/game/UserSettingsKeybinds.test.ts | 22 +++++++++++++++ 3 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 tests/core/game/UserSettingsKeybinds.test.ts diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index c2b1556659..f1854471cd 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -521,7 +521,7 @@ export class InputHandler { this.eventBus.emit(new CenterCameraEvent()); } - if (e.code === this.keybinds.selectAllWarships) { + if (this.keybindMatchesEvent(e, this.keybinds.selectAllWarships)) { e.preventDefault(); this.eventBus.emit(new SelectAllWarshipsEvent()); } @@ -879,17 +879,34 @@ export class InputHandler { return { shift: false, alt: false, code: value }; } + private static readonly MODIFIER_KEY_CODES = new Set([ + "ShiftLeft", + "ShiftRight", + "AltLeft", + "AltRight", + "ControlLeft", + "ControlRight", + "MetaLeft", + "MetaRight", + ]); + /** * Returns true if the keyboard event matches the given keybind value, * including optional Shift+ and Alt+ prefix support. */ private keybindMatchesEvent(e: KeyboardEvent, keybindValue: string): boolean { const parsed = this.parseKeybind(keybindValue); - return ( - e.code === parsed.code && - e.shiftKey === parsed.shift && - e.altKey === parsed.alt - ); + if (e.code !== parsed.code) return false; + // A bare modifier-key bind (e.g. "ShiftLeft") can't be matched on flag + // state: pressing the key itself sets its own modifier flag to true. + if ( + !parsed.shift && + !parsed.alt && + InputHandler.MODIFIER_KEY_CODES.has(parsed.code) + ) { + return true; + } + return e.shiftKey === parsed.shift && e.altKey === parsed.alt; } private static readonly MOUSE_BUTTON_NAMES: Record = { diff --git a/tests/InputHandler.test.ts b/tests/InputHandler.test.ts index 3da402e6f2..db8a47b523 100644 --- a/tests/InputHandler.test.ts +++ b/tests/InputHandler.test.ts @@ -949,7 +949,7 @@ describe("Warship box selection (Shift+drag)", () => { pointerId: 1, }), ); - inputHandler["activeKeys"].add("ShiftLeft"); + window.dispatchEvent(new KeyboardEvent("keydown", { code: "ShiftLeft" })); inputHandler["onPointerMove"]( new PointerEvent("pointermove", { button: 0, @@ -981,7 +981,7 @@ describe("Warship box selection (Shift+drag)", () => { pointerId: 1, }), ); - inputHandler["activeKeys"].add("ShiftLeft"); + window.dispatchEvent(new KeyboardEvent("keydown", { code: "ShiftLeft" })); inputHandler["onPointerMove"]( new PointerEvent("pointermove", { button: 0, @@ -1032,7 +1032,7 @@ describe("Warship box selection (Shift+drag)", () => { pointerId: 1, }), ); - inputHandler["activeKeys"].add("ShiftLeft"); + window.dispatchEvent(new KeyboardEvent("keydown", { code: "ShiftLeft" })); inputHandler["onPointerMove"]( new PointerEvent("pointermove", { button: 0, diff --git a/tests/core/game/UserSettingsKeybinds.test.ts b/tests/core/game/UserSettingsKeybinds.test.ts new file mode 100644 index 0000000000..91b0ceb10b --- /dev/null +++ b/tests/core/game/UserSettingsKeybinds.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "vitest"; +import { getDefaultKeybinds } from "../../../src/core/game/UserSettings"; + +describe("getDefaultKeybinds", () => { + test("resetGfx defaults to Alt+KeyR", () => { + expect(getDefaultKeybinds(false).resetGfx).toBe("Alt+KeyR"); + expect(getDefaultKeybinds(true).resetGfx).toBe("Alt+KeyR"); + }); + + test("selectAllWarships defaults to KeyF", () => { + expect(getDefaultKeybinds(false).selectAllWarships).toBe("KeyF"); + }); + + test("shiftKey (warship box select) defaults to ShiftLeft", () => { + expect(getDefaultKeybinds(false).shiftKey).toBe("ShiftLeft"); + }); + + test("modifierKey is platform aware", () => { + expect(getDefaultKeybinds(false).modifierKey).toBe("ControlLeft"); + expect(getDefaultKeybinds(true).modifierKey).toBe("MetaLeft"); + }); +});