Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
4 changes: 2 additions & 2 deletions src/client/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
21 changes: 21 additions & 0 deletions src/client/KeybindsModal.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -32,6 +34,8 @@ const DefaultKeybinds: Record<string, string> = {

@customElement("keybinds-modal")
export class KeybindsModal extends BaseModal {
private userSettings: UserSettings = new UserSettings();

@state() private keybinds: Record<
string,
{ value: string | string[]; key: string }
Expand Down Expand Up @@ -386,6 +390,23 @@ export class KeybindsModal extends BaseModal {
${translateText("user_setting.attack_ratio_controls")}
</h2>

<setting-dropdown
label=${translateText("user_setting.attack_ratio_increment_label")}
description=${translateText("user_setting.attack_ratio_increment_desc")}
.options=${[
{ value: "0.01", label: "1%" },
{ value: "0.025", label: "2.5%" },
{ value: "0.05", label: "5%" },
{ value: "0.1", label: "10%" },
{ value: "0.2", label: "20%" },
]}
value=${this.userSettings.attackRatioIncrement().toString()}
@change=${(e: CustomEvent) => {
this.userSettings.setAttackRatioIncrement(parseFloat(e.detail.value));
this.requestUpdate();
}}
></setting-dropdown>

<setting-keybind
action="attackRatioDown"
label=${translateText("user_setting.attack_ratio_down")}
Expand Down
82 changes: 82 additions & 0 deletions src/client/components/baseComponents/setting/SettingDropdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";

@customElement("setting-dropdown")
export class SettingDropdown extends LitElement {
@property() label = "Setting";
@property() description = "";
@property() id = "";
@property({ type: Array }) options: { value: string; label: string }[] = [];
@property() value = "";

createRenderRoot() {
return this;
}

private handleChange(e: Event) {
const select = e.target as HTMLSelectElement;
this.value = select.value;
this.dispatchEvent(
new CustomEvent("change", {
detail: { value: this.value },
bubbles: true,
composed: true,
}),
);
}

render() {
return html`
<label
class="flex flex-row items-center justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-4 cursor-pointer"
>
<div class="flex flex-col flex-1 min-w-0 mr-4">
<div class="text-white font-bold text-base block mb-1">
${this.label}
</div>
<div class="text-white/50 text-sm leading-snug">
${this.description}
</div>
</div>

<div class="relative inline-block w-auto shrink-0">
<select
id=${this.id}
class="bg-black/40 text-white border border-white/20 rounded-lg px-3 py-1.5 focus:outline-none focus:border-blue-500 transition-colors cursor-pointer appearance-none pr-8"
@change=${this.handleChange}
.value=${this.value}
>
${this.options.map(
(option) => html`
<option
value=${option.value}
class="bg-gray-900 text-white"
?selected=${option.value === this.value}
>
${option.label}
</option>
`,
)}
</select>
<div
class="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none text-white/50"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</div>
</label>
`;
}
}
20 changes: 15 additions & 5 deletions src/client/graphics/layers/ControlPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/core/game/UserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
36 changes: 36 additions & 0 deletions tests/InputHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
);
});
});
});
});
45 changes: 45 additions & 0 deletions tests/core/game/UserSettings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { UserSettings } from "../../../src/core/game/UserSettings";

describe("UserSettings", () => {
let userSettings: UserSettings;
let mockStorage: Record<string, string> = {};

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");
});
});
20 changes: 19 additions & 1 deletion tests/setup.ts
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
// Add global mocks or configuration here if needed
const mockStorage: Record<string, string> = {};

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,
});
Loading