Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
179ff4b
Reworked InputHandler
TKTK123456 Jun 11, 2026
9c1594d
Removed comments that had previous code
TKTK123456 Jun 11, 2026
3145844
Added build keybinds to keybindAndEvent map
TKTK123456 Jun 11, 2026
b8ed5c0
Quick bug fix and format
TKTK123456 Jun 11, 2026
9425280
Merge branch 'openfrontio:main' into InputHandler.ts-Rework
TKTK123456 Jun 11, 2026
d6b9cd9
Fix eslint issue
TKTK123456 Jun 11, 2026
fe048ec
Ran formatter
TKTK123456 Jun 12, 2026
d8e52eb
Added numpad compatibility to buildkeybinds
TKTK123456 Jun 12, 2026
d65b727
Removed debugging console log
TKTK123456 Jun 12, 2026
e698885
Merge branch 'openfrontio:main' into InputHandler.ts-Rework
TKTK123456 Jun 12, 2026
3879620
Formatted
TKTK123456 Jun 12, 2026
156a016
Added shift compatibility to build keybinds
TKTK123456 Jun 12, 2026
f7ac39f
Merge branch 'openfrontio:main' into InputHandler.ts-Rework
TKTK123456 Jun 12, 2026
33ea404
Merge branch 'main' into InputHandler.ts-Rework
TKTK123456 Jun 12, 2026
88e9d36
Added digits to buildKeybinds and made sure its unique.
TKTK123456 Jun 13, 2026
b2b0e95
Added keybind entry interface to keybindAndEvent
TKTK123456 Jun 13, 2026
b7b0cb7
Removed all undifined from buildKeybinds array
TKTK123456 Jun 13, 2026
e694d94
Merge branch 'main' into InputHandler.ts-Rework
TKTK123456 Jun 13, 2026
d6ca322
Formated
TKTK123456 Jun 13, 2026
bd9a0a2
Fixed eslint issue
TKTK123456 Jun 13, 2026
106d02d
Fixed initialization of keybindAndEvent
TKTK123456 Jun 13, 2026
ae55fa7
Fixed typo
TKTK123456 Jun 13, 2026
7a9e8aa
Removed allConditionsFulfilled to instead break early
TKTK123456 Jun 13, 2026
050b5e1
Formated
TKTK123456 Jun 14, 2026
ce03021
Added altKey for graphics refresh modifer rebindability
TKTK123456 Jun 14, 2026
8913f16
Sorted en.json
TKTK123456 Jun 14, 2026
c063585
Removing debug console log
TKTK123456 Jun 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
2 changes: 2 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,8 @@
"game_speed_up_desc": "Cycle to next game speed (0.5, 1, 2, max). Single player only.",
"go_to_player_desc": "Toggle zooming in on the player in the beginning of a game.",
"go_to_player_label": "Go to player on start",
"graphics_refresh_modifier": "Graphics refresh modifier",
"graphics_refresh_modifier_desc": "Hold this key and R to refresh graphics",
"graphics_settings_desc": "Adjust how the map looks",
"graphics_settings_label": "Graphics Settings",
"ground_attack": "Ground Attack",
Expand Down
304 changes: 170 additions & 134 deletions src/client/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ export class TickMetricsEvent implements GameEvent {
) {}
}

interface KeybindEntry {
handler: (e: KeyboardEvent) => void;
conditions: Array<(e: KeyboardEvent) => boolean>;
}

export class InputHandler {
private lastPointerX: number = 0;
private lastPointerY: number = 0;
Expand Down Expand Up @@ -222,6 +227,7 @@ export class InputHandler {
private moveInterval: NodeJS.Timeout | null = null;
private activeKeys = new Set<string>();
private keybinds: Record<string, string> = {};
private keybindAndEvent: Array<[string, KeybindEntry]> = [];
private coordinateGridEnabled = false;

private readonly PAN_SPEED = 5;
Expand All @@ -240,6 +246,138 @@ export class InputHandler {
initialize() {
this.keybinds = this.userSettings.keybinds(Platform.isMac);

this.addKeybindAndEvent(this.keybinds.boatAttack, () => {
this.eventBus.emit(new DoBoatAttackEvent());
});
this.addKeybindAndEvent(this.keybinds.groundAttack, () => {
this.eventBus.emit(new DoGroundAttackEvent());
});
this.addKeybindAndEvent(this.keybinds.retaliateAttack, () => {
this.eventBus.emit(new DoRetaliateAttackEvent());
});
this.addKeybindAndEvent(this.keybinds.centerCamera, () => {
this.eventBus.emit(new CenterCameraEvent());
});
this.addKeybindAndEvent(this.keybinds.selectAllWarships, () => {
this.eventBus.emit(new SelectAllWarshipsEvent());
});
this.addKeybindAndEvent(this.keybinds.requestAlliance, () => {
this.eventBus.emit(new DoRequestAllianceEvent());
});
this.addKeybindAndEvent(this.keybinds.breakAlliance, () => {
this.eventBus.emit(new DoBreakAllianceEvent());
});
this.addKeybindAndEvent(
this.keybinds.pauseGame,
() => {
this.eventBus.emit(new TogglePauseIntentEvent());
},
(e: KeyboardEvent) => !e.repeat,
);
this.addKeybindAndEvent(
this.keybinds.gameSpeedUp,
() => {
this.eventBus.emit(new GameSpeedUpIntentEvent());
},
(e: KeyboardEvent) => !e.repeat,
);
this.addKeybindAndEvent(
this.keybinds.gameSpeedDown,
() => {
this.eventBus.emit(new GameSpeedDownIntentEvent());
},
(e: KeyboardEvent) => !e.repeat,
);
this.addKeybindAndEvent(this.keybinds.attackRatioDown, () => {
const increment = this.userSettings.attackRatioIncrement();
this.eventBus.emit(new AttackRatioEvent(-increment));
});
this.addKeybindAndEvent(this.keybinds.attackRatioUp, () => {
const increment = this.userSettings.attackRatioIncrement();
this.eventBus.emit(new AttackRatioEvent(increment));
});
this.addKeybindAndEvent(this.keybinds.swapDirection, () => {
const nextDirection = !this.uiState.rocketDirectionUp;
this.eventBus.emit(new SwapRocketDirectionEvent(nextDirection));
});
this.addKeybindAndEvent("Shift+KeyD", () => {
this.eventBus.emit(new TogglePerformanceOverlayEvent());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
this.addKeybindAndEvent(this.keybinds.toggleView, () => {
this.alternateView = false;
this.eventBus.emit(new AlternateViewEvent(false));
});
const resetKey = this.keybinds.resetGfx ?? "KeyR";
this.addKeybindAndEvent(
resetKey,
() => {
this.eventBus.emit(new RefreshGraphicsEvent());
},
(e: KeyboardEvent) => this.activeKeys.has(this.keybinds.altKey),
);

let buildKeybinds: string[] = [
"buildCity",
"buildFactory",
"buildPort",
"buildDefensePost",
"buildMissileSilo",
"buildSamLauncher",
"buildAtomBomb",
"buildHydrogenBomb",
"buildWarship",
"buildMIRV",
];
buildKeybinds = buildKeybinds.map((i: string): string => {
return this.keybinds[i];
});
buildKeybinds.push(
...[
"Numpad0",
"Numpad1",
"Numpad2",
"Numpad3",
"Numpad4",
"Numpad5",
"Numpad6",
"Numpad7",
"Numpad8",
"Numpad9",
"Digit0",
"Digit1",
"Digit2",
"Digit3",
"Digit4",
"Digit5",
"Digit6",
"Digit7",
"Digit8",
"Digit9",
],
);
buildKeybinds.push(
...buildKeybinds.map((t) => {
return "Shift+" + t;
}),
);
buildKeybinds = [...new Set(buildKeybinds)].filter(
(v): v is string => v !== null,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
for (const i of buildKeybinds) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
this.addKeybindAndEvent(
i,
(e: KeyboardEvent) => {
const matchedBuild = this.resolveBuildKeybind(e.code, e.shiftKey);

if (matchedBuild !== null) {
this.setGhostStructure(matchedBuild);
}
},
() => this.canUseBuildKeybinds(),
(e: KeyboardEvent) =>
this.resolveBuildKeybind(e.code, e.shiftKey) !== null,
);
}
// Listen for warship selection to change cursor
this.eventBus.on(UnitSelectionEvent, (e) => {
if (e.isSelected && (e.units ?? []).length > 0) {
Expand Down Expand Up @@ -438,6 +576,7 @@ export class InputHandler {
this.keybinds.shiftKey,
this.keybinds.emojiMenuModifier,
this.keybinds.buildMenuModifier,
this.keybinds.altKey,
].includes(e.code)
) {
this.activeKeys.add(e.code);
Expand Down Expand Up @@ -476,103 +615,17 @@ export class InputHandler {
this.activeKeys.delete(this.keybinds.zoomOut);
}

if (this.keybindMatchesEvent(e, this.keybinds.toggleView)) {
e.preventDefault();
this.alternateView = false;
this.eventBus.emit(new AlternateViewEvent(false));
}

const resetKey = this.keybinds.resetGfx ?? "KeyR";
if (e.code === resetKey && this.isAltKeyHeld(e)) {
e.preventDefault();
this.eventBus.emit(new RefreshGraphicsEvent());
}

if (this.keybindMatchesEvent(e, this.keybinds.boatAttack)) {
e.preventDefault();
this.eventBus.emit(new DoBoatAttackEvent());
}

if (this.keybindMatchesEvent(e, this.keybinds.groundAttack)) {
e.preventDefault();
this.eventBus.emit(new DoGroundAttackEvent());
}

if (this.keybindMatchesEvent(e, this.keybinds.retaliateAttack)) {
e.preventDefault();
this.eventBus.emit(new DoRetaliateAttackEvent());
}

if (this.keybindMatchesEvent(e, this.keybinds.attackRatioDown)) {
e.preventDefault();
const increment = this.userSettings.attackRatioIncrement();
this.eventBus.emit(new AttackRatioEvent(-increment));
}

if (this.keybindMatchesEvent(e, this.keybinds.attackRatioUp)) {
e.preventDefault();
const increment = this.userSettings.attackRatioIncrement();
this.eventBus.emit(new AttackRatioEvent(increment));
}

if (this.keybindMatchesEvent(e, this.keybinds.centerCamera)) {
e.preventDefault();
this.eventBus.emit(new CenterCameraEvent());
}

if (e.code === this.keybinds.selectAllWarships) {
e.preventDefault();
this.eventBus.emit(new SelectAllWarshipsEvent());
}

// Two-phase build keybind matching: exact code match first, then digit/Numpad alias.
if (this.canUseBuildKeybinds()) {
const matchedBuild = this.resolveBuildKeybind(e.code, e.shiftKey);
if (matchedBuild !== null) {
outerLoop: for (const item of this.keybindAndEvent) {
if (this.keybindMatchesEvent(e, item[0])) {
for (const i of item[1].conditions) {
if (!i(e)) {
continue outerLoop;
}
}
e.preventDefault();
this.setGhostStructure(matchedBuild);
item[1].handler(e);
}
}

if (this.keybindMatchesEvent(e, this.keybinds.requestAlliance)) {
e.preventDefault();
this.eventBus.emit(new DoRequestAllianceEvent());
}

if (this.keybindMatchesEvent(e, this.keybinds.breakAlliance)) {
e.preventDefault();
this.eventBus.emit(new DoBreakAllianceEvent());
}

if (this.keybindMatchesEvent(e, this.keybinds.swapDirection)) {
e.preventDefault();
const nextDirection = !this.uiState.rocketDirectionUp;
this.eventBus.emit(new SwapRocketDirectionEvent(nextDirection));
}

if (!e.repeat && this.keybindMatchesEvent(e, this.keybinds.pauseGame)) {
e.preventDefault();
this.eventBus.emit(new TogglePauseIntentEvent());
}
if (!e.repeat && this.keybindMatchesEvent(e, this.keybinds.gameSpeedUp)) {
e.preventDefault();
this.eventBus.emit(new GameSpeedUpIntentEvent());
}
if (
!e.repeat &&
this.keybindMatchesEvent(e, this.keybinds.gameSpeedDown)
) {
e.preventDefault();
this.eventBus.emit(new GameSpeedDownIntentEvent());
}

// Shift-D to toggle performance overlay
if (e.code === "KeyD" && e.shiftKey) {
e.preventDefault();
console.log("TogglePerformanceOverlayEvent");
this.eventBus.emit(new TogglePerformanceOverlayEvent());
}

this.activeKeys.delete(e.code);

// Reset crosshair when Shift is released (unless selection box or multi-selection still active)
Expand Down Expand Up @@ -867,7 +920,10 @@ export class InputHandler {
* Returns true if the keyboard event matches the given keybind value,
* including optional Shift+ prefix support.
*/
private keybindMatchesEvent(e: KeyboardEvent, keybindValue: string): boolean {
private keybindMatchesEvent(
e: KeyboardEvent | { shiftKey: boolean; code: string },
keybindValue: string,
): boolean {
const parsed = this.parseKeybind(keybindValue);
return e.code === parsed.code && e.shiftKey === parsed.shift;
}
Expand All @@ -893,16 +949,6 @@ export class InputHandler {
return null;
}

/** Strict equality only: used for first-pass exact KeyboardEvent.code match. */
private buildKeybindMatches(
code: string,
shiftKey: boolean,
keybindValue: string,
): boolean {
const parsed = this.parseKeybind(keybindValue);
return code === parsed.code && shiftKey === parsed.shift;
}

/** Digit/Numpad alias match: used only when no exact match was found. */
private buildKeybindMatchesDigit(
code: string,
Expand All @@ -916,6 +962,24 @@ export class InputHandler {
return digit !== null && bindDigit !== null && digit === bindDigit;
}

/**
* Add a keybind that activates on one press
* @param keybind The keybind that is being activated
* @param event The code to be exectued when this keybind is pressed
* @param conditions Optional conditions that can be added, they get the keyboard up event passed to them
*/
private addKeybindAndEvent(
keybind: string,
event: (type: KeyboardEvent) => any,
...conditions: ((type: KeyboardEvent) => any)[]
) {
const entry: KeybindEntry = {
handler: event,
conditions,
};
this.keybindAndEvent.push([keybind, entry]);
}

/**
* Resolves a keyup code to a build action: exact code match first, then digit/Numpad alias.
* Returns the UnitType to set as ghost, or null if no build keybind matched.
Expand All @@ -940,7 +1004,7 @@ export class InputHandler {
{ key: "buildMIRV", type: UnitType.MIRV },
];
for (const { key, type } of buildKeybinds) {
if (this.buildKeybindMatches(code, shiftKey, this.keybinds[key]))
if (this.keybindMatchesEvent({ code, shiftKey }, this.keybinds[key]))
return type;
}
for (const { key, type } of buildKeybinds) {
Expand Down Expand Up @@ -992,32 +1056,4 @@ export class InputHandler {
}
this.activeKeys.clear();
}

private isAltKeyHeld(event: KeyboardEvent): boolean {
if (
this.keybinds.altKey === "AltLeft" ||
this.keybinds.altKey === "AltRight"
) {
return event.altKey && !event.ctrlKey;
}
if (
this.keybinds.altKey === "ControlLeft" ||
this.keybinds.altKey === "ControlRight"
) {
return event.ctrlKey;
}
if (
this.keybinds.altKey === "ShiftLeft" ||
this.keybinds.altKey === "ShiftRight"
) {
return event.shiftKey;
}
if (
this.keybinds.altKey === "MetaLeft" ||
this.keybinds.altKey === "MetaRight"
) {
return event.metaKey;
}
return false;
}
}
Loading
Loading