diff --git a/resources/lang/en.json b/resources/lang/en.json index 290353f6a6..d30c293d87 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -492,6 +492,8 @@ "attack_ratio_up_desc": "Increase attack ratio by 10%", "attack_ratio_down": "Decrease Attack Ratio", "attack_ratio_down_desc": "Decrease attack ratio by 10%", + "attack_ratio_increment_label": "Attack Ratio Increment", + "attack_ratio_increment_desc": "The amount to increase/decrease the attack ratio by.", "attack_keybinds": "Attack Keybinds", "boat_attack": "Boat Attack", "boat_attack_desc": "Send a boat attack to the tile under your cursor.", diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 949da78d5b..fdc250349d 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -377,12 +377,12 @@ export class InputHandler { if (e.code === this.keybinds.attackRatioDown) { e.preventDefault(); - this.eventBus.emit(new AttackRatioEvent(-10)); + this.eventBus.emit(new AttackRatioEvent(-1)); } if (e.code === this.keybinds.attackRatioUp) { e.preventDefault(); - this.eventBus.emit(new AttackRatioEvent(10)); + this.eventBus.emit(new AttackRatioEvent(1)); } if (e.code === this.keybinds.centerCamera) { diff --git a/src/client/KeybindsModal.ts b/src/client/KeybindsModal.ts index 2eadfd4c4d..c9da80c1d0 100644 --- a/src/client/KeybindsModal.ts +++ b/src/client/KeybindsModal.ts @@ -1,6 +1,8 @@ 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/SettingDropdown"; import "./components/baseComponents/setting/SettingKeybind"; import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind"; import { BaseModal } from "./components/BaseModal"; @@ -32,6 +34,8 @@ const DefaultKeybinds: Record = { @customElement("keybinds-modal") export class KeybindsModal extends BaseModal { + private userSettings: UserSettings = new UserSettings(); + @state() private keybinds: Record< string, { value: string | string[]; key: string } @@ -386,6 +390,23 @@ export class KeybindsModal extends BaseModal { ${translateText("user_setting.attack_ratio_controls")} + { + this.userSettings.setAttackRatioIncrement(parseFloat(e.detail.value)); + this.requestUpdate(); + }} + > + +
+
+ ${this.label} +
+
+ ${this.description} +
+
+ +
+ +
+ + + +
+
+ + `; + } +} diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index cbe0ef70c2..91e1d02832 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -4,6 +4,7 @@ import { translateText } from "../../../client/Utils"; 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"; @@ -17,6 +18,8 @@ export class ControlPanel extends LitElement implements Layer { public eventBus: EventBus; public uiState: UIState; + private userSettings = new UserSettings(); + @state() private attackRatio: number = 0.2; @@ -45,12 +48,19 @@ export class ControlPanel extends LitElement implements Layer { ); this.uiState.attackRatio = this.attackRatio; this.eventBus.on(AttackRatioEvent, (event) => { - let newAttackRatio = - (parseInt( + const currentRatio = + parseInt( (document.getElementById("attack-ratio") as HTMLInputElement).value, - ) + - event.attackRatio) / - 100; + ) / 100; + + const direction = Math.sign(event.attackRatio); + if (direction === 0) return; + + const increment = this.userSettings.attackRatioIncrement(); + let newAttackRatio = currentRatio + direction * increment; + + // to avoid floating point weirdness (e.g. 0.300000004) + newAttackRatio = Math.round(newAttackRatio * 1000) / 1000; if (newAttackRatio < 0.01) { newAttackRatio = 0.01; diff --git a/src/core/game/UserSettings.ts b/src/core/game/UserSettings.ts index ba74b9ae8c..289ee48cac 100644 --- a/src/core/game/UserSettings.ts +++ b/src/core/game/UserSettings.ts @@ -207,4 +207,12 @@ export class UserSettings { setSoundEffectsVolume(volume: number): void { this.setFloat("settings.soundEffectsVolume", volume); } + + attackRatioIncrement(): number { + return this.getFloat("settings.attackRatioIncrement", 0.1); + } + + setAttackRatioIncrement(value: number): void { + this.setFloat("settings.attackRatioIncrement", value); + } } diff --git a/tests/InputHandler.test.ts b/tests/InputHandler.test.ts index 132e884190..579d678954 100644 --- a/tests/InputHandler.test.ts +++ b/tests/InputHandler.test.ts @@ -451,5 +451,41 @@ describe("InputHandler AutoUpgrade", () => { expect((inputHandler as any).keybinds.moveUp).toBe("KeyW"); spy.mockRestore(); }); + + describe("Attack Ratio Keybinds", () => { + test("should emit AttackRatioEvent with 1 when up key is pressed", () => { + const mockEmit = vi.spyOn(eventBus, "emit"); + inputHandler.initialize(); + + const keyEvent = new KeyboardEvent("keyup", { + code: "KeyY", + }); + + window.dispatchEvent(keyEvent); + + expect(mockEmit).toHaveBeenCalledWith( + expect.objectContaining({ + attackRatio: 1, + }), + ); + }); + + test("should emit AttackRatioEvent with -1 when down key is pressed", () => { + const mockEmit = vi.spyOn(eventBus, "emit"); + inputHandler.initialize(); + + const keyEvent = new KeyboardEvent("keyup", { + code: "KeyT", + }); + + window.dispatchEvent(keyEvent); + + expect(mockEmit).toHaveBeenCalledWith( + expect.objectContaining({ + attackRatio: -1, + }), + ); + }); + }); }); }); diff --git a/tests/core/game/UserSettings.test.ts b/tests/core/game/UserSettings.test.ts new file mode 100644 index 0000000000..4f9dffb48c --- /dev/null +++ b/tests/core/game/UserSettings.test.ts @@ -0,0 +1,45 @@ +import { UserSettings } from "../../../src/core/game/UserSettings"; + +describe("UserSettings", () => { + let userSettings: UserSettings; + let mockStorage: Record = {}; + + beforeAll(() => { + Object.defineProperty(window, "localStorage", { + value: { + getItem: (key: string) => mockStorage[key] || null, + setItem: (key: string, value: string) => { + mockStorage[key] = value.toString(); + }, + removeItem: (key: string) => { + delete mockStorage[key]; + }, + clear: () => { + mockStorage = {}; + }, + }, + writable: true, + }); + }); + + beforeEach(() => { + mockStorage = {}; + // Ensure clean state even if UserSettings caches something (it doesn't, it reads from LS) + userSettings = new UserSettings(); + }); + + test("attackRatioIncrement returns default 0.1", () => { + expect(userSettings.attackRatioIncrement()).toBe(0.1); + }); + + test("setAttackRatioIncrement sets and retrieves value", () => { + userSettings.setAttackRatioIncrement(0.05); + expect(userSettings.attackRatioIncrement()).toBe(0.05); + }); + + test("setAttackRatioIncrement persists to localStorage", () => { + userSettings.setAttackRatioIncrement(0.025); + const stored = localStorage.getItem("settings.attackRatioIncrement"); + expect(stored).toBe("0.025"); + }); +}); diff --git a/tests/setup.ts b/tests/setup.ts index 940d2d5241..3404c3c09b 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1 +1,19 @@ -// Add global mocks or configuration here if needed +const mockStorage: Record = {}; + +Object.defineProperty(window, "localStorage", { + value: { + getItem: (key: string) => mockStorage[key] || null, + setItem: (key: string, value: string) => { + mockStorage[key] = value.toString(); + }, + removeItem: (key: string) => { + delete mockStorage[key]; + }, + clear: () => { + for (const key in mockStorage) { + delete mockStorage[key]; + } + }, + }, + writable: true, +});