Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
10b908b
Made the multiplier for buildings and nukes scale linearly instead of…
Pesinario Sep 6, 2026
889cc0c
Refactored via guard clause to reduce nesting
Pesinario Sep 8, 2026
a5788b8
Implemented alt+scroll as single number increase/decrease for valid g…
Pesinario Sep 8, 2026
1ff87e2
clamp upgradeMultiplier to 50 & refactored to a switch statement
Pesinario Sep 8, 2026
40f0f7c
ran prettier, linting
Pesinario Sep 9, 2026
302d171
Improved Alt key prevention tests
Pesinario Sep 10, 2026
9fdaf76
Removed leftover logic from previous branch
Pesinario Sep 10, 2026
18bdeba
Added comments clarifying intended behavior.
Pesinario Sep 10, 2026
297055c
Added tests for intended behavior
Pesinario Sep 10, 2026
7295d72
fix: Potentially some keyboards could get their rightAlt blocked
Pesinario Sep 10, 2026
d016e85
Cleaned up setGhostStructure logic
Pesinario Sep 10, 2026
360ee47
Refactors scroll events for improved mantainability.
Pesinario Sep 11, 2026
f0148fd
Adds a test to ensure no regression on shift + scroll behavior
Pesinario Sep 11, 2026
7c2ce30
Micro lint
Pesinario Sep 11, 2026
e69a40c
fix: alt (potentially) did not block zoom on minimal scroll events
Pesinario Sep 11, 2026
21fec22
Ensures consistent alt prevention behavior on keydown and keyup.
Pesinario Sep 11, 2026
56e9b94
fix: lint
Pesinario Sep 11, 2026
d19d6e9
fix: wrong comparison target
Pesinario Sep 11, 2026
761681e
looser alt prevention
Pesinario Sep 11, 2026
3f7e4b7
improves tests, additional tests for rebound keybinds
Pesinario Sep 11, 2026
b367176
Merge branch 'main' into feat/multiplier_changes
Pesinario Sep 11, 2026
7867eec
Changes behavior to respond to a keybind instead of raw key
Pesinario Sep 13, 2026
493866b
Allows changing keybind from settings
Pesinario Sep 13, 2026
3f87964
Moves tests to own describe statement and changes them for keybind-ba…
Pesinario Sep 13, 2026
56e06cf
Strict AltLeft prevention
Pesinario Sep 13, 2026
714e113
Prevent zoom if build scroll modifier OR any ALT is pressed
Pesinario Sep 13, 2026
2c72e82
Removes tests that never made any sense to begin with
Pesinario Sep 13, 2026
9b78edd
fix: Period missing.
Pesinario Sep 13, 2026
d4adace
Stronger tests via rebinding to non-modifier key
Pesinario Sep 13, 2026
38b8d9c
micro lint
Pesinario Sep 14, 2026
bdf1f31
fix: triple share alt rebinding
Pesinario Sep 14, 2026
ddde1d3
Added alt-sharing keybinds to existing default keybinds test.
Pesinario Sep 14, 2026
41ef190
Makes test behavior clearer to AI.
Pesinario Sep 14, 2026
10e9cdb
Update comment to better describe intended behavior
Pesinario Sep 14, 2026
6cbcbd1
Fix: AI was right after all
Pesinario Sep 14, 2026
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
108 changes: 80 additions & 28 deletions src/client/InputHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { EventBus, GameEvent } from "../core/EventBus";
import { PlayerBuildableUnitType, UnitType } from "../core/game/Game";
import {
MAX_UPGRADE_AMOUNT,
PlayerBuildableUnitType,
UnitType,
} from "../core/game/Game";
import { UserSettings } from "../core/game/UserSettings";
import { Platform } from "./Platform";
import { UIState } from "./UIState";
Expand Down Expand Up @@ -456,6 +460,7 @@ export class InputHandler {
(e) => {
this.onScroll(e);
this.onShiftScroll(e);
this.onAltScroll(e);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
e.preventDefault();
},
{ passive: false },
Expand Down Expand Up @@ -579,6 +584,10 @@ export class InputHandler {
return;
}

if (e.altKey || e.code === this.keybinds.altKey) {
e.preventDefault();
}

if (this.keybindMatchesEvent(e, this.keybinds.toggleView)) {
e.preventDefault();
if (!this.alternateView) {
Expand Down Expand Up @@ -696,6 +705,10 @@ export class InputHandler {
return;
}

if (e.altKey || e.code === this.keybinds.altKey) {
e.preventDefault();
}

// When the meta (cmd) or ctrl key is released, any keys that were held
// simultaneously will have had their keyup swallowed by the browser
// (e.g. cmd+Plus for browser zoom). Clear zoom-related keys to
Expand Down Expand Up @@ -850,7 +863,9 @@ export class InputHandler {
}
if (this.activeKeys.has(this.keybinds.emojiMenuModifier)) {
this.suppressNextTap = false;
if (this.uiState.ghostStructure === null) {
this.eventBus.emit(new ShowEmojiMenuEvent(event.clientX, event.clientY));
}
return;
}

Expand Down Expand Up @@ -883,32 +898,33 @@ export class InputHandler {
}

private onScroll(event: WheelEvent) {
if (!event.shiftKey) {
const realCtrl =
this.activeKeys.has("ControlLeft") ||
this.activeKeys.has("ControlRight");
if (event.ctrlKey) {
if (!realCtrl) {
// Pinch-to-zoom gesture (trackpad): small deltas, amplify.
// Ignore large deltas — those are browser zoom shortcuts (cmd+/cmd-)
// which fire synthetic wheel events we don't want to handle.
if (Math.abs(event.deltaY) <= 10) {
this.eventBus.emit(
new ZoomEvent(event.x, event.y, event.deltaY * 10),
);
}
if (event.shiftKey || event.altKey){
return; // Shift/Alt scroll is handled separately
}
const realCtrl =
this.activeKeys.has("ControlLeft") ||
this.activeKeys.has("ControlRight");
if (event.ctrlKey) {
if (!realCtrl) {
// Pinch-to-zoom gesture (trackpad): small deltas, amplify.
// Ignore large deltas — those are browser zoom shortcuts (cmd+/cmd-)
// which fire synthetic wheel events we don't want to handle.
if (Math.abs(event.deltaY) <= 10) {
this.eventBus.emit(
new ZoomEvent(event.x, event.y, event.deltaY * 10),
);
}
// Always return when ctrlKey is set — whether it's a real ctrl scroll,
// a pinch gesture, or a browser zoom event, none should reach the
// regular scroll path below.
return;
}
// Regular scroll wheel: ignore tiny residual momentum events that macOS
// keeps sending after a gesture ends (especially after browser zoom changes
// devicePixelRatio, which can cause these to accumulate into runaway zoom).
if (Math.abs(event.deltaY) < 2) return;
this.eventBus.emit(new ZoomEvent(event.x, event.y, event.deltaY));
// Always return when ctrlKey is set — whether it's a real ctrl scroll,
// a pinch gesture, or a browser zoom event, none should reach the
// regular scroll path below.
return;
}
// Regular scroll wheel: ignore tiny residual momentum events that macOS
// keeps sending after a gesture ends (especially after browser zoom changes
// devicePixelRatio, which can cause these to accumulate into runaway zoom).
if (Math.abs(event.deltaY) < 2) return;
this.eventBus.emit(new ZoomEvent(event.x, event.y, event.deltaY));
}

/**
Expand Down Expand Up @@ -943,6 +959,15 @@ export class InputHandler {
}
}

private onAltScroll(event: WheelEvent) {
if (event.altKey) {
const scrollValue = event.deltaY === 0 ? event.deltaX : event.deltaY;
this.setGhostStructure(this.uiState.ghostStructure,
scrollValue > 0 ? "decrease" : "increase");
}
}


private onPointerMove(event: PointerEvent) {
if (event.button === 1) {
event.preventDefault();
Expand Down Expand Up @@ -1029,13 +1054,40 @@ export class InputHandler {
this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY));
}

private setGhostStructure(ghostStructure: PlayerBuildableUnitType | null) {
if (
private setGhostStructure(
ghostStructure: PlayerBuildableUnitType | null,
source: "increase" | "decrease" | "hotkey" = "hotkey",
) { if (
this.uiState.ghostStructure === ghostStructure &&
ghostStructure !== null
) {
this.uiState.upgradeMultiplier =
this.uiState.upgradeMultiplier === 1 ? 5 : 1;
const currentMultiplier = this.uiState.upgradeMultiplier ?? 1;
switch (source) {
case "hotkey":
this.uiState.upgradeMultiplier =
currentMultiplier === 1 ? 5 : currentMultiplier + 5;
break;
case "increase":
this.uiState.upgradeMultiplier = currentMultiplier + 1;
break;
case "decrease":
this.uiState.upgradeMultiplier =
currentMultiplier > 1 ? currentMultiplier - 1 : 1;
break;
}
if (this.uiState.upgradeMultiplier > MAX_UPGRADE_AMOUNT) {
if (source === "hotkey") {
// Reset to 1 if hotkey was used to exceed max, otherwise clamp to max
this.uiState.upgradeMultiplier = 1;
} else {
this.uiState.upgradeMultiplier = MAX_UPGRADE_AMOUNT;
}
}
if (source === "decrease"){
this.uiState.upgradeMultiplier =
currentMultiplier > 1 ? currentMultiplier - 1 : 1;
}

} else {
this.uiState.upgradeMultiplier = 1;
this.uiState.ghostStructure = ghostStructure;
Expand Down
28 changes: 28 additions & 0 deletions tests/InputHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,20 @@ describe("InputHandler AutoUpgrade", () => {
});
});

describe("Alt key default prevention", () => {
test("prevents the browser's default action when Alt is pressed", () => {
const preventDefaultSpy = vi.spyOn(
KeyboardEvent.prototype,
"preventDefault",
);

window.dispatchEvent(new KeyboardEvent("keydown", { code: "AltLeft" }));

expect(preventDefaultSpy).toHaveBeenCalled();
preventDefaultSpy.mockRestore();
Comment thread
Pesinario marked this conversation as resolved.
Outdated
});
});

describe("Numpad number keys for build keybinds", () => {
beforeEach(() => {
inputHandler.destroy();
Expand Down Expand Up @@ -708,6 +722,20 @@ describe("InputHandler AutoUpgrade", () => {

expect(inputHandler["uiState"].ghostStructure).toBeNull();
});

test("repeated taps increase the build multiplier by 5 each time", () => {
const uiState = inputHandler["uiState"];

inputHandler["setGhostStructure"](UnitType.AtomBomb);
expect(uiState.ghostStructure).toBe(UnitType.AtomBomb);
expect(uiState.upgradeMultiplier).toBe(1);

inputHandler["setGhostStructure"](UnitType.AtomBomb);
expect(uiState.upgradeMultiplier).toBe(5);

inputHandler["setGhostStructure"](UnitType.AtomBomb);
expect(uiState.upgradeMultiplier).toBe(10);
});
});

describe("Digit keys still set ghost structure when bound to Numpad", () => {
Expand Down
Loading