diff --git a/src/client/HelpModal.ts b/src/client/HelpModal.ts index f791d13f1..261a0b0d7 100644 --- a/src/client/HelpModal.ts +++ b/src/client/HelpModal.ts @@ -2,6 +2,7 @@ import { html } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import { translateText, TUTORIAL_VIDEO_URL } from "../client/Utils"; import { assetUrl } from "../core/AssetUrls"; +import { UserSettings } from "../core/game/UserSettings"; import { BaseModal } from "./components/BaseModal"; import "./components/Difficulties"; import { modalHeader } from "./components/ui/ModalHeader"; @@ -13,57 +14,8 @@ export class HelpModal extends BaseModal { @state() private keybinds: Record = this.getKeybinds(); @query("#tutorial-video-iframe") private videoIframe?: HTMLIFrameElement; - private isKeybindObject(v: unknown): v is { value: string } { - return ( - typeof v === "object" && - v !== null && - "value" in v && - typeof (v as any).value === "string" - ); - } - private getKeybinds(): Record { - let saved: Record = {}; - try { - const parsed = JSON.parse( - localStorage.getItem("settings.keybinds") ?? "{}", - ); - saved = Object.fromEntries( - Object.entries(parsed) - .map(([k, v]) => { - if (this.isKeybindObject(v)) return [k, v.value]; - if (typeof v === "string") return [k, v]; - return [k, undefined]; - }) - .filter(([, v]) => typeof v === "string" && v !== "Null"), - ) as Record; - } catch (e) { - console.warn("Invalid keybinds JSON:", e); - } - - const isMac = Platform.isMac; - return { - toggleView: "Space", - coordinateGrid: "KeyM", - centerCamera: "KeyC", - moveUp: "KeyW", - moveDown: "KeyS", - moveLeft: "KeyA", - moveRight: "KeyD", - zoomOut: "KeyQ", - zoomIn: "KeyE", - attackRatioDown: "KeyT", - attackRatioUp: "KeyY", - swapDirection: "KeyU", - shiftKey: "ShiftLeft", - modifierKey: isMac ? "MetaLeft" : "ControlLeft", - altKey: "AltLeft", - resetGfx: "KeyR", - pauseGame: "KeyP", - gameSpeedUp: "Period", - gameSpeedDown: "Comma", - ...saved, - }; + return new UserSettings().keybinds(Platform.isMac); } private getKeyLabel(code: string): string { diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 253cea83c..21638fcd0 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -184,70 +184,7 @@ export class InputHandler { ) {} initialize() { - let saved: Record = {}; - try { - const parsed = JSON.parse( - localStorage.getItem("settings.keybinds") ?? "{}", - ); - // flatten { key: {key, value} } → { key: value } and accept legacy string values - saved = Object.fromEntries( - Object.entries(parsed) - .map(([k, v]) => { - // Extract value from nested object or plain string - let val: unknown; - if (v && typeof v === "object" && "value" in v) { - val = (v as { value: unknown }).value; - } else { - val = v; - } - - // Map invalid values to undefined (filtered later) - if (typeof val !== "string") { - return [k, undefined]; - } - return [k, val]; - }) - .filter(([, v]) => typeof v === "string"), - ) as Record; - } catch (e) { - console.warn("Invalid keybinds JSON:", e); - } - - // Mac users might have different keybinds - const isMac = Platform.isMac; - - this.keybinds = { - toggleView: "Space", - coordinateGrid: "KeyM", - centerCamera: "KeyC", - moveUp: "KeyW", - moveDown: "KeyS", - moveLeft: "KeyA", - moveRight: "KeyD", - zoomOut: "KeyQ", - zoomIn: "KeyE", - attackRatioDown: "KeyT", - attackRatioUp: "KeyY", - boatAttack: "KeyB", - groundAttack: "KeyG", - swapDirection: "KeyU", - modifierKey: isMac ? "MetaLeft" : "ControlLeft", - altKey: "AltLeft", - buildCity: "Digit1", - buildFactory: "Digit2", - buildPort: "Digit3", - buildDefensePost: "Digit4", - buildMissileSilo: "Digit5", - buildSamLauncher: "Digit6", - buildWarship: "Digit7", - buildAtomBomb: "Digit8", - buildHydrogenBomb: "Digit9", - buildMIRV: "Digit0", - pauseGame: "KeyP", - gameSpeedUp: "Period", - gameSpeedDown: "Comma", - ...saved, - }; + this.keybinds = this.userSettings.keybinds(Platform.isMac); this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e)); window.addEventListener("pointerup", (e) => this.onPointerUp(e)); diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 9914c66d2..220051b7f 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -1,7 +1,7 @@ import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { formatKeyForDisplay, translateText } from "../client/Utils"; -import { UserSettings } from "../core/game/UserSettings"; +import { getDefaultKeybinds, UserSettings } from "../core/game/UserSettings"; import "./components/baseComponents/setting/SettingKeybind"; import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind"; import "./components/baseComponents/setting/SettingNumber"; @@ -12,52 +12,19 @@ import { BaseModal } from "./components/BaseModal"; import { modalHeader } from "./components/ui/ModalHeader"; import { Platform } from "./Platform"; -const isMac = Platform.isMac; - -const DefaultKeybinds: Record = { - toggleView: "Space", - coordinateGrid: "KeyM", - 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", - pauseGame: "KeyP", - gameSpeedUp: "Period", - gameSpeedDown: "Comma", -}; - @customElement("user-setting") export class UserSettingModal extends BaseModal { private userSettings: UserSettings = new UserSettings(); + private readonly defaultKeybinds = getDefaultKeybinds(Platform.isMac); @state() private activeTab: "basic" | "keybinds" = "basic"; @state() private keySequence: string[] = []; @state() private showEasterEggSettings = false; - @state() private keybinds: Record< + @state() private userKeybinds: Record< string, - { value: string | string[]; key: string } + { value: string; key: string } > = {}; connectedCallback() { @@ -71,55 +38,39 @@ export class UserSettingModal extends BaseModal { } private loadKeybindsFromStorage() { - const savedKeybinds = this.userSettings.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); + 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( @@ -132,11 +83,9 @@ export class UserSettingModal extends BaseModal { ) { 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; + const activeKeybinds = { ...this.defaultKeybinds }; + for (const [k, v] of Object.entries(this.userKeybinds)) { + const normalizedValue = v.value; if (normalizedValue === "Null") { delete activeKeybinds[k]; } else { @@ -188,32 +137,33 @@ export class UserSettingModal extends BaseModal { }), ); - const element = this.renderRoot.querySelector( + const element = this.renderRoot.querySelector( `setting-keybind[action="${action}"]`, - ) as SettingKeybind; + ); if (element) { - element.value = prevValue ?? DefaultKeybinds[action] ?? ""; + element.value = prevValue ?? this.defaultKeybinds[action] ?? ""; element.requestUpdate(); } return; } - this.keybinds = { ...this.keybinds, [action]: { value: value, key: key } }; - this.userSettings.setKeybinds(JSON.stringify(this.keybinds)); + this.userKeybinds = { + ...this.userKeybinds, + [action]: { value: value, key: key }, + }; + this.userSettings.setKeybinds(this.userKeybinds); } private getKeyValue(action: string): string | undefined { - const entry = this.keybinds[action]; + const entry = this.userKeybinds[action]; if (!entry) return undefined; - const normalizedValue = Array.isArray(entry.value) - ? entry.value[0] || "" - : entry.value; + const normalizedValue = entry.value; if (normalizedValue === "Null") return ""; return normalizedValue || undefined; } private getKeyChar(action: string): string { - const entry = this.keybinds[action]; + const entry = this.userKeybinds[action]; if (!entry) return ""; return entry.key || ""; } @@ -453,7 +403,7 @@ export class UserSettingModal extends BaseModal { action="coordinateGrid" label=${translateText("user_setting.coordinate_grid_label")} description=${translateText("user_setting.coordinate_grid_desc")} - defaultKey=${DefaultKeybinds.coordinateGrid} + defaultKey=${this.defaultKeybinds.coordinateGrid} .value=${this.getKeyValue("coordinateGrid")} .display=${this.getKeyChar("coordinateGrid")} @change=${this.handleKeybindChange} @@ -575,7 +525,7 @@ export class UserSettingModal extends BaseModal { action="modifierKey" label=${translateText("user_setting.build_menu_modifier")} description=${translateText("user_setting.build_menu_modifier_desc")} - .defaultKey=${DefaultKeybinds.modifierKey} + .defaultKey=${this.defaultKeybinds.modifierKey} .value=${this.getKeyValue("modifierKey")} .display=${this.getKeyChar("modifierKey")} @change=${this.handleKeybindChange} @@ -585,7 +535,7 @@ export class UserSettingModal extends BaseModal { action="altKey" label=${translateText("user_setting.emoji_menu_modifier")} description=${translateText("user_setting.emoji_menu_modifier_desc")} - .defaultKey=${DefaultKeybinds.altKey} + .defaultKey=${this.defaultKeybinds.altKey} .value=${this.getKeyValue("altKey")} .display=${this.getKeyChar("altKey")} @change=${this.handleKeybindChange} @@ -595,7 +545,7 @@ export class UserSettingModal extends BaseModal { action="pauseGame" label=${translateText("user_setting.pause_game")} description=${translateText("user_setting.pause_game_desc")} - .defaultKey=${DefaultKeybinds.pauseGame} + .defaultKey=${this.defaultKeybinds.pauseGame} .value=${this.getKeyValue("pauseGame")} .display=${this.getKeyChar("pauseGame")} @change=${this.handleKeybindChange} @@ -605,7 +555,7 @@ export class UserSettingModal extends BaseModal { action="gameSpeedUp" label=${translateText("user_setting.game_speed_up")} description=${translateText("user_setting.game_speed_up_desc")} - .defaultKey=${DefaultKeybinds.gameSpeedUp} + .defaultKey=${this.defaultKeybinds.gameSpeedUp} .value=${this.getKeyValue("gameSpeedUp")} .display=${this.getKeyChar("gameSpeedUp")} @change=${this.handleKeybindChange} @@ -615,7 +565,7 @@ export class UserSettingModal extends BaseModal { action="gameSpeedDown" label=${translateText("user_setting.game_speed_down")} description=${translateText("user_setting.game_speed_down_desc")} - .defaultKey=${DefaultKeybinds.gameSpeedDown} + .defaultKey=${this.defaultKeybinds.gameSpeedDown} .value=${this.getKeyValue("gameSpeedDown")} .display=${this.getKeyChar("gameSpeedDown")} @change=${this.handleKeybindChange} @@ -681,7 +631,7 @@ export class UserSettingModal extends BaseModal { action="swapDirection" label=${translateText("user_setting.swap_direction")} description=${translateText("user_setting.swap_direction_desc")} - .defaultKey=${DefaultKeybinds.swapDirection} + .defaultKey=${this.defaultKeybinds.swapDirection} .value=${this.getKeyValue("swapDirection")} .display=${this.getKeyChar("swapDirection")} @change=${this.handleKeybindChange} diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index 033e34771..81fbd7124 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -4,6 +4,7 @@ import { assetUrl } from "../../../core/AssetUrls"; import { EventBus } from "../../../core/EventBus"; import { Gold } from "../../../core/game/Game"; import { GameView } from "../../../core/game/GameView"; +import { UserSettings } from "../../../core/game/UserSettings"; import { ClientID } from "../../../core/Schemas"; import { AttackRatioEvent } from "../../InputHandler"; import { renderNumber, renderTroops } from "../../Utils"; @@ -50,9 +51,7 @@ export class ControlPanel extends LitElement implements Layer { } init() { - this.attackRatio = Number( - localStorage.getItem("settings.attackRatio") ?? "0.2", - ); + this.attackRatio = new UserSettings().attackRatio(); this.uiState.attackRatio = this.attackRatio; this.eventBus.on(AttackRatioEvent, (event) => { let newAttackRatio = this.attackRatio + event.attackRatio / 100; diff --git a/src/client/graphics/layers/UnitDisplay.ts b/src/client/graphics/layers/UnitDisplay.ts index 88f965f98..29831ab3d 100644 --- a/src/client/graphics/layers/UnitDisplay.ts +++ b/src/client/graphics/layers/UnitDisplay.ts @@ -10,6 +10,7 @@ import { UnitType, } from "../../../core/game/Game"; import { GameView } from "../../../core/game/GameView"; +import { UserSettings } from "../../../core/game/UserSettings"; import { GhostStructureChangedEvent, ToggleStructureEvent, @@ -52,15 +53,9 @@ export class UnitDisplay extends LitElement implements Layer { init() { const config = this.game.config(); + const userSettings = new UserSettings(); - const savedKeybinds = localStorage.getItem("settings.keybinds"); - if (savedKeybinds) { - try { - this.keybinds = JSON.parse(savedKeybinds); - } catch (e) { - console.warn("Invalid keybinds JSON:", e); - } - } + this.keybinds = userSettings.parsedUserKeybinds(); this.allDisabled = BuildMenus.types.every((u) => config.isUnitDisabled(u)); this.requestUpdate(); diff --git a/src/core/game/UserSettings.ts b/src/core/game/UserSettings.ts index e077f4079..158d10894 100644 --- a/src/core/game/UserSettings.ts +++ b/src/core/game/UserSettings.ts @@ -1,12 +1,49 @@ import { Cosmetics } from "../CosmeticSchemas"; import { PlayerPattern } from "../Schemas"; +export function getDefaultKeybinds(isMac: boolean): Record { + return { + toggleView: "Space", + coordinateGrid: "KeyM", + 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", + shiftKey: "ShiftLeft", + resetGfx: "KeyR", + pauseGame: "KeyP", + gameSpeedUp: "Period", + gameSpeedDown: "Comma", + }; +} + export const USER_SETTINGS_CHANGED_EVENT = "event:user-settings-changed"; export const PATTERN_KEY = "territoryPattern"; export const FLAG_KEY = "flag"; export const COLOR_KEY = "settings.territoryColor"; export const DARK_MODE_KEY = "settings.darkMode"; export const PERFORMANCE_OVERLAY_KEY = "settings.performanceOverlay"; +export const KEYBINDS_KEY = "settings.keybinds"; export class UserSettings { private static cache = new Map(); @@ -40,7 +77,7 @@ export class UserSettings { } } - private removeCached(key: string, emitChange: boolean = true) { + public removeCached(key: string, emitChange: boolean = true) { localStorage.removeItem(key); UserSettings.cache.set(key, null); if (emitChange) { @@ -283,12 +320,63 @@ export class UserSettings { this.setFloat("settings.attackRatio", value); } - keybinds(): string { - return this.getString("settings.keybinds", ""); + // In case localStorage was manually edited to be invalid, return an empty object + parsedUserKeybinds(): Record { + const raw = this.getString(KEYBINDS_KEY, "{}"); + try { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed; + } + } catch (e) { + console.warn("Invalid keybinds JSON:", e); + } + return {}; } - setKeybinds(value: string): void { - this.setString("settings.keybinds", value); + // Returns a flat keybind map { action: "keyCode" }, handling nested objects and legacy strings + private normalizedUserKeybinds(): Record { + const parsed = this.parsedUserKeybinds(); + return Object.fromEntries( + Object.entries(parsed) + // Extract value from nested object or plain string, filter out non-string values + .map(([k, v]) => { + let val = v; + if (v && typeof v === "object" && !Array.isArray(v) && "value" in v) { + val = v.value; + } + if (Array.isArray(val) && typeof val[0] === "string") { + val = val[0]; + } + return [k, val]; + }) + .filter(([, v]) => typeof v === "string"), + ) as Record; + } + + keybinds(isMac: boolean): Record { + const merged = { + ...getDefaultKeybinds(isMac), + ...this.normalizedUserKeybinds(), + }; + // Actually unbind key: if Unbind is clicked in UserSettingsModal, eg. for Attack Ratio Up, + // keybind is "Null". Even if it is in default kindbinds (Y), it should not work anymore. + // The key (Y) can now be bound to another action like Boat Attack, and no two actions listen to the same key. + for (const k in merged) { + if (merged[k] === "Null") { + delete merged[k]; + } + } + + return merged; + } + + setKeybinds(value: string | Record): void { + if (typeof value === "string") { + this.setString(KEYBINDS_KEY, value); + } else { + this.setString(KEYBINDS_KEY, JSON.stringify(value)); + } } soundEffectsVolume(): number { diff --git a/tests/InputHandler.test.ts b/tests/InputHandler.test.ts index cc5446bc4..a9bd708f9 100644 --- a/tests/InputHandler.test.ts +++ b/tests/InputHandler.test.ts @@ -7,6 +7,7 @@ import { UIState } from "../src/client/graphics/UIState"; import { EventBus } from "../src/core/EventBus"; import { UnitType } from "../src/core/game/Game"; import { GameView } from "../src/core/game/GameView"; +import { KEYBINDS_KEY, UserSettings } from "../src/core/game/UserSettings"; class MockPointerEvent { button: number; @@ -39,8 +40,12 @@ describe("InputHandler AutoUpgrade", () => { let mockGameView: GameView; let eventBus: EventBus; let mockCanvas: HTMLCanvasElement; + let testSettings: UserSettings; beforeEach(() => { + testSettings = new UserSettings(); + testSettings.removeCached(KEYBINDS_KEY, false); + mockGameView = { inSpawnPhase: () => false } as GameView; mockCanvas = document.createElement("canvas"); mockCanvas.width = 800; @@ -476,15 +481,11 @@ describe("InputHandler AutoUpgrade", () => { }); describe("Keybinds JSON parsing", () => { - beforeEach(() => { - localStorage.removeItem("settings.keybinds"); - }); - test("parses nested object values and flattens them to strings", () => { const nested = { moveUp: { key: "moveUp", value: "KeyZ" }, }; - localStorage.setItem("settings.keybinds", JSON.stringify(nested)); + testSettings.setKeybinds(nested); inputHandler.initialize(); @@ -492,33 +493,30 @@ describe("InputHandler AutoUpgrade", () => { }); test("accepts legacy string values", () => { - localStorage.setItem( - "settings.keybinds", - JSON.stringify({ moveUp: "KeyX" }), - ); + testSettings.setKeybinds({ moveUp: "KeyX" }); inputHandler.initialize(); expect((inputHandler as any).keybinds.moveUp).toBe("KeyX"); }); - test("ignores non-string values and preserves defaults, but keeps 'Null' for unbound keys", () => { + test("ignores non-string values and preserves defaults, removes 'Null' for unbound keys", () => { const mixed = { moveUp: { key: "moveUp", value: null }, moveLeft: "Null", }; - localStorage.setItem("settings.keybinds", JSON.stringify(mixed)); + testSettings.setKeybinds(mixed); inputHandler.initialize(); expect((inputHandler as any).keybinds.moveUp).toBe("KeyW"); - // "Null" is preserved to indicate unbound keybind - expect((inputHandler as any).keybinds.moveLeft).toBe("Null"); + // "Null" entries are removed entirely to indicate unbound keybind + expect((inputHandler as any).keybinds.moveLeft).toBeUndefined(); }); test("handles invalid JSON gracefully and warns", () => { const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); - localStorage.setItem("settings.keybinds", "not a json"); + testSettings.setKeybinds("not a json"); inputHandler.initialize(); @@ -533,7 +531,6 @@ describe("InputHandler AutoUpgrade", () => { let uiState: UIState; beforeEach(() => { - localStorage.removeItem("settings.keybinds"); uiState = { attackRatio: 20, ghostStructure: null, @@ -589,7 +586,6 @@ describe("InputHandler AutoUpgrade", () => { describe("Numpad number keys for build keybinds", () => { beforeEach(() => { - localStorage.removeItem("settings.keybinds"); inputHandler.destroy(); const uiState: UIState = { attackRatio: 20, @@ -631,7 +627,6 @@ describe("InputHandler AutoUpgrade", () => { describe("Build keybind two-phase matching (exact code first, then digit/Numpad alias)", () => { beforeEach(() => { - localStorage.removeItem("settings.keybinds"); inputHandler.destroy(); const uiState: UIState = { attackRatio: 20, @@ -650,13 +645,10 @@ describe("InputHandler AutoUpgrade", () => { }); test("exact code match wins: Digit1 sets City when buildCity=Digit1 and buildFactory=Numpad1", () => { - localStorage.setItem( - "settings.keybinds", - JSON.stringify({ - buildCity: "Digit1", - buildFactory: "Numpad1", - }), - ); + testSettings.setKeybinds({ + buildCity: "Digit1", + buildFactory: "Numpad1", + }); inputHandler.destroy(); const uiState: UIState = { attackRatio: 20, @@ -681,13 +673,10 @@ describe("InputHandler AutoUpgrade", () => { }); test("exact code match wins: Numpad1 sets Factory when buildCity=Digit1 and buildFactory=Numpad1", () => { - localStorage.setItem( - "settings.keybinds", - JSON.stringify({ - buildCity: "Digit1", - buildFactory: "Numpad1", - }), - ); + testSettings.setKeybinds({ + buildCity: "Digit1", + buildFactory: "Numpad1", + }); inputHandler.destroy(); const uiState: UIState = { attackRatio: 20, @@ -712,10 +701,7 @@ describe("InputHandler AutoUpgrade", () => { }); test("digit alias used when no exact match: Numpad1 sets City when only buildCity=Digit1", () => { - localStorage.setItem( - "settings.keybinds", - JSON.stringify({ buildCity: "Digit1" }), - ); + testSettings.setKeybinds({ buildCity: "Digit1" }); inputHandler.destroy(); const uiState: UIState = { attackRatio: 20,