Skip to content

WIP#3735

Draft
VariableVince wants to merge 5 commits intomainfrom
keys-wrongly-displayed
Draft

WIP#3735
VariableVince wants to merge 5 commits intomainfrom
keys-wrongly-displayed

Conversation

@VariableVince
Copy link
Copy Markdown
Contributor

If this PR fixes an issue, link it below. If not, delete these two lines.
Resolves #(issue number)

Description:

WIP DRAFT

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

DISCORD_USERNAME

@VariableVince VariableVince self-assigned this Apr 21, 2026
@VariableVince VariableVince added Refactor Code cleanup, technical debt, refactoring, and architecture improvements. Bugfix Fixes a bug labels Apr 21, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Refactors keybind handling to use a typed KeybindAction enum, centralizes keybind matching (including optional Shift+ parsing), adds keyboard-layout-aware key formatting with a cached getLayoutMap(), and updates UI/locale strings and tests to the new APIs.

Changes

Cohort / File(s) Summary
Core Keybind Types & Storage
src/core/game/UserSettings.ts
Add KeybindAction enum and KeyUnbound sentinel; move defaults to enum keys; replace setKeybinds with setUserKeybinds and add removeUserKeybinds; make removeCached private; change parsing/normalization to enum-keyed records.
Type Declarations
src/client/vite-env.d.ts
Add optional navigator.keyboard.getLayoutMap() typing to Navigator interface.
Input Handling & Matching
src/client/InputHandler.ts, tests/InputHandler.test.ts
Refactor to Record<KeybindAction, string> keybinds; centralize matching via keybindMatchesEvent() with Shift+ support; replace Alt/AltPressed helpers with isMenuModifierPressed(); update tests to new API and sentinels.
Key Display & Utils
src/client/Utils.ts, src/client/Main.ts
Add cached async getKeyboardLayoutMap(); extend formatKeyForDisplay() to use layout map, handle Shift+ prefixes and physical key fallbacks; eagerly prefetch layout in Client.initialize().
Setting Capture & UI Components
src/client/components/baseComponents/setting/SettingKeybind.ts, src/client/UserSettingModal.ts, src/client/HelpModal.ts, src/client/graphics/layers/UnitDisplay.ts
Make action typed KeybindAction; use KeyUnbound for unbound keys; change display logic to emit formatted/display strings; switch UI wiring to typed actions and formatKeyForDisplay(); add keybind hint banner; update HelpModal hotkey rows to new modifier field names.
Localization
resources/lang/en.json
Clarify user_setting.build_menu_modifier_desc and user_setting.emoji_menu_modifier_desc to specify the modifier opens the legacy build menu / emoji menu and note “No Shift.”

Sequence Diagram

sequenceDiagram
    actor User as User
    participant IH as InputHandler
    participant US as UserSettings
    participant Utils as Utils
    participant Nav as Navigator.keyboard

    User->>IH: press key (maybe Shift)
    IH->>US: read keybinds (KeybindAction → bind string)
    IH->>IH: parseKeybind(bindString) / keybindMatchesEvent(event)
    alt match found
        IH->>IH: execute action (game behavior)
    else no match
        IH-->>User: no action
    end

    Note over Utils,Nav: key formatting/caching
    Utils->>Nav: getLayoutMap() [once]
    Nav-->>Utils: Map<string,string>
    Utils->>Utils: cache map
    IH->>Utils: formatKeyForDisplay(code or "Shift+code")
    Utils-->>IH: human-friendly label
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • evanpelle

Poem

🎹 Keys now speak in Shift and code,
Enums replace the old string road,
Layout maps whisper each key's name,
Typed binds steady the binding game,
A cached map sings — the labels glow.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The pull request title 'WIP' is vague and generic, providing no meaningful information about the changeset's purpose or scope. Replace 'WIP' with a descriptive title that summarizes the main change, such as 'Refactor keybinding system to use typed enum keys and keyboard layout formatting' or similar.
Description check ❓ Inconclusive The pull request description is a draft template with placeholder text and incomplete checklist items, not describing the actual implementation. Complete the description by removing placeholder text, linking the resolved issue, and describing the substantive keybinding system changes being made.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/client/graphics/layers/UnitDisplay.ts (1)

129-219: ⚠️ Potential issue | 🟡 Minor

Use the shared key formatter for unit hotkeys.

The unit bar still formats hotkeys with .replace(...).toUpperCase(), so new values like Shift+BracketLeft or layout-mapped keys can render as SHIFT+BRACKETLEFT instead of the same label shown in settings/help.

🐛 Proposed fix
-import { renderNumber, translateText } from "../../Utils";
+import { formatKeyForDisplay, renderNumber, translateText } from "../../Utils";
@@
-    const displayHotkey = hotkey
-      .replace("Digit", "")
-      .replace("Key", "")
-      .toUpperCase();
+    const displayHotkey = formatKeyForDisplay(hotkey);

Based on learnings: displayKey values are rendered through formatKeyForDisplay(), which already handles fallback capitalization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/UnitDisplay.ts` around lines 129 - 219, The hotkey
string in renderUnitItem is being manually transformed with
.replace(...).toUpperCase(), which misformats complex keys; replace that logic
by using the shared formatter function formatKeyForDisplay(hotkey) (or the
existing displayKey helper used elsewhere) to compute displayHotkey so unit
hotkeys match the labels shown in settings/help and handle combos like
Shift+BracketLeft correctly; update references in renderUnitItem where
displayHotkey is computed and used to call formatKeyForDisplay instead of the
chained replace/toUpperCase.
src/client/components/baseComponents/setting/SettingKeybind.ts (1)

95-106: ⚠️ Potential issue | 🟡 Minor

Finish reserved-key handling before accepting binds.

Enter and arrow keys can still be saved, but InputHandler always uses them for ghost confirm and camera movement. That can create double actions. Please reserve/block them here or model them in a typed reserved-key list; also remove the debug log.

I can help generate the reserved-key guard if you want to track it as a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/baseComponents/setting/SettingKeybind.ts` around lines
95 - 106, Add a reserved-key guard in SettingKeybind's keydown handler so Enter
and arrow keys (ArrowUp, ArrowDown, ArrowLeft, ArrowRight, "Enter") are treated
like Tab/Escape: don't accept them as binds and cancel listening when
appropriate (use the existing this.listening flow and requestUpdate()), or
reference a typed reserved-key enum in UserSettings/InputHandler if one exists;
also remove the debug console.log("Keydown event:", e) line. Ensure the handler
prevents saving these reserved keys while still allowing other keys to be bound,
and keep compatibility with InputHandler's existing ghost-confirm and camera
movement behavior.
src/client/InputHandler.ts (1)

222-276: ⚠️ Potential issue | 🟠 Major

Support or block Shift+ binds for hold actions.

Settings now allow Shift+key, but movement/zoom tracking still compares raw e.code and Line 226 exits whenever Shift is held. So Shift+W for move/zoom-style actions is accepted by the UI but never works. Either block Shift+ for these hold actions in settings, or parse Shift-aware active keys here.

Also applies to: 328-356

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/InputHandler.ts` around lines 222 - 276, The movement/zoom loop
(moveInterval) currently returns early whenever ShiftLeft/ShiftRight are down,
which prevents Shift+key binds from working; remove that early-return and change
the key checks to be Shift-aware by checking both the raw code and a
Shift-prefixed variant when Shift is held (e.g. if Shift active treat a bind
like "W" as matching either "KeyW" or "Shift+KeyW"); update the comparisons that
reference this.activeKeys, this.keybinds.moveUp/moveDown/moveLeft/moveRight and
this.keybinds.zoomIn/zoomOut so they test both the base code and the Shift+code
(or detect bind strings that include "Shift+"), then emit DragEvent/ZoomEvent as
before using PAN_SPEED/ZOOM_SPEED and eventBus.emit.
🧹 Nitpick comments (8)
src/client/Utils.ts (1)

333-335: Remove temporary key-format logging.

formatKeyForDisplay() is called by settings/help rendering, so this test log will spam the console for normal UI use.

🧹 Proposed cleanup
 export function formatKeyForDisplay(value: string): string {
-  // TODO remove after testing
-  console.info("formatKeyForDisplay recieved: " + value);
-
   // Handle empty string

I can help remove the temporary debug path before merge if you want.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/Utils.ts` around lines 333 - 335, Remove the temporary debug
logging from formatKeyForDisplay: delete the console.info("formatKeyForDisplay
recieved: " + value); line (and the "TODO remove after testing" comment) in the
formatKeyForDisplay function in Utils.ts so the function no longer spams the
console during normal UI rendering; leave the function body to perform its
intended formatting and return the formatted string.
src/client/vite-env.d.ts (1)

41-44: Use ReadonlyMap<string, string> instead of mutable Map.

The browser's KeyboardLayoutMap is read-only and provides: get(), has(), size, forEach(), entries(), keys(), values(), and iterator support. Typing as Promise<Map<string, string>> is wider than the actual API and misleads callers into thinking they can use mutable methods like set/delete/clear. Use ReadonlyMap<string, string> to match the actual API and follow idiomatic TypeScript.

♻️ Proposed type correction
 interface Navigator {
   keyboard?: {
-    getLayoutMap(): Promise<Map<string, string>>;
+    getLayoutMap(): Promise<ReadonlyMap<string, string>>;
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/vite-env.d.ts` around lines 41 - 44, The
Navigator.keyboard.getLayoutMap() signature currently returns
Promise<Map<string, string>> but the browser API is read-only; change the return
type to Promise<ReadonlyMap<string, string>> so callers can't assume mutating
methods (set/delete/clear) exist. Update the declaration for the Navigator
interface's keyboard property and its getLayoutMap() method to use
ReadonlyMap<string, string> (and import/ensure ReadonlyMap is available in the
ambient types) so the type matches KeyboardLayoutMap.
src/client/UserSettingModal.ts (2)

100-113: Small readability nit: action is shadowed inside the loop.

The outer action from e.detail is shadowed by the loop variable for (const [action, codeAndKey] of Object.entries(...)). It happens to work because the outer action is used again only after the loop (on line 112 / 156), but the shadowing makes the code harder to follow. Renaming the loop variable keeps things obvious and avoids future bugs if anyone adds code inside the loop that expects the outer action.

♻️ Suggested rename
     const activeKeybinds = { ...this.defaultKeybinds };
-    for (const [action, codeAndKey] of Object.entries(this.userKeybinds)) {
-      const normalizedCode = codeAndKey.value;
+    for (const [boundAction, codeAndKey] of Object.entries(this.userKeybinds)) {
+      const normalizedCode = codeAndKey.value;
       if (normalizedCode === KeyUnbound) {
-        delete activeKeybinds[action];
+        delete activeKeybinds[boundAction];
       } else {
-        activeKeybinds[action] = normalizedCode;
+        activeKeybinds[boundAction] = normalizedCode;
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/UserSettingModal.ts` around lines 100 - 113, The loop variable
shadows the outer action (from e.detail), so rename the loop binding in the
for-of that iterates Object.entries(this.userKeybinds) (currently "for (const
[action, codeAndKey] ...)") to a non-conflicting name like "keybindAction" or
"userAction" and update its usages inside the loop (and any related references
such as codeAndKey and normalizedCode) so the outer action remains unshadowed;
ensure this change is made where activeKeybinds is constructed and where codes
is computed so behavior is unchanged.

51-79: Unknown action names from localStorage will silently land in userKeybinds.

validated[action] uses action straight from Object.entries(parsed) — any leftover/typo action from an older version of the app will be kept as a Partial<Record<KeybindAction, ...>> key. It will never match any render site and will never be cleaned up. Consider filtering unknown actions so the storage stays in sync with the enum:

♻️ Suggested filter
     for (const [action, entry] of Object.entries(parsed)) {
+      if (!(action in this.defaultKeybinds)) continue; // drop unknown actions
       if (typeof entry === "string") {
         validated[action] = { value: entry, key: entry };
       } else if (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/UserSettingModal.ts` around lines 51 - 79, During validation in
the loop over Object.entries(parsed) you currently assign validated[action]
directly which preserves unknown/typo keys; change the loop so you first verify
that action is a known KeybindAction (e.g. check it exists in the KeybindAction
enum or in your canonical keybind map like defaultKeybinds) and only then
populate validated[action]; discard any entries where the action is not
recognized so userKeybinds only contains valid KeybindAction keys.
src/core/game/UserSettings.ts (3)

4-10: Typos/clarity in the reserved-keys comment, and the KeyUnbound sentinel could use a docstring.

A couple of small cleanups to help non-native readers and future maintainers:

  • Line 5: "Shift+left quick" → "Shift+left click".
  • Line 6: "kindbinds" → "keybinds".
  • Line 7: "alltogether" → "altogether".
  • KeyUnbound = "Null" is a string that travels through JSON.stringify into localStorage and is then compared with ===. That is fine, but the capital-N "Null" is very easy to confuse with null. A short JSDoc on the constant explaining "sentinel stored in JSON to mark an explicitly unbound action; do NOT use null" would save the next reader a trip through the call sites.
📝 Suggested edits
-// ADD Reserved keys here or in Utils.ts:
-// (maybe also comment about Shift+left quick being reserved for future devs, see HelpModal, code in onPointerUp in InputHandler: when leftClickOpensMenu is false, Shift+left click is hardcoded to be attack. So it should not be used elsewhere where modifier+click is expected)
-// Shift+D for performance overlay, Alt+R for reset gfx (NO not reserved anymore, can be changed with fallback removed), Escape for menu close and cancel ghost build, Enter for confirm Ghost build
-// But also browser combos: Ctrl+Shift+I for dev tools, etc. Shift+Tab for backwards tabbing through fields. Alt+N for new browser screen, etc. If we won't just suppress Alt/Ctrl etc alltogether or if used in combination (but then confusing things could still happen)
-// These keys won't be available for binding in UserSettingsModal.
+// Reserved keys (add new ones here or in Utils.ts):
+// - Shift+left click: reserved for future use. See HelpModal and onPointerUp in InputHandler:
+//   when leftClickOpensMenu is false, Shift+left click is hardcoded as "attack", so it must
+//   not be reused for anything else where modifier+click is expected.
+// - Shift+D: performance overlay.
+// - Alt+R: reset gfx (not strictly reserved anymore; can be changed if the fallback is removed).
+// - Escape: close menu / cancel ghost build.
+// - Enter: confirm ghost build.
+// Browser combos are also effectively reserved: Ctrl+Shift+I (dev tools), Shift+Tab
+// (reverse focus), Alt+N (new window), etc. Unless we suppress Alt/Ctrl entirely, binding
+// these can still produce confusing behavior.
+// These keys are not selectable in UserSettingsModal.

-export const KeyUnbound = "Null";
+/**
+ * Sentinel stored (as a plain string) in the keybinds JSON to mark an action as
+ * explicitly unbound by the user. Compared with `===` on the `code` string.
+ * Note: this is the string "Null", not the JavaScript `null` — the value must
+ * survive `JSON.stringify`/`JSON.parse` round-trips.
+ */
+export const KeyUnbound = "Null";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/UserSettings.ts` around lines 4 - 10, Fix the typos and clarify
the reserved-keys comment (change "Shift+left quick" → "Shift+left click",
"kindbinds" → "keybinds", "alltogether" → "altogether", and tidy phrasing about
browser combos/reservations) and add a short JSDoc above the exported KeyUnbound
constant (export const KeyUnbound = "Null") explaining that this is a
JSON/string sentinel used to mark an explicitly unbound action (do NOT use null,
it is intentionally the string "Null" and is compared with ===), so future
readers aren’t confused by the capitalized "Null".

406-421: for...in over mergedKeybinds — small nit.

for (const action in mergedKeybinds) iterates string keys and typescript loses the KeybindAction typing on action. Object.entries keeps the code idiomatic and the types tighter:

♻️ Suggested tweak
-    for (const action in mergedKeybinds) {
-      if (mergedKeybinds[action] === KeyUnbound) {
-        delete mergedKeybinds[action];
-      }
-    }
+    for (const [action, code] of Object.entries(mergedKeybinds)) {
+      if (code === KeyUnbound) {
+        delete mergedKeybinds[action as KeybindAction];
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/UserSettings.ts` around lines 406 - 421, The loop in
keybinds(isMac) uses for...in which yields string-typed keys and loses the
KeybindAction typing; replace that loop with Object.entries(mergedKeybinds) to
iterate [action, key] pairs, type action as KeybindAction (or cast each entry to
[KeybindAction, string]), and remove entries where key === KeyUnbound (or build
a new Record by filtering entries) so mergedKeybinds retains proper typing and
the same unbind behavior; refer to functions getDefaultKeybinds,
normalizedUserKeybinds, and the constant KeyUnbound to locate the code to
change.

368-404: Return types are a bit optimistic — consider filtering to known actions.

parsedUserKeybinds() claims Partial<Record<KeybindAction, any>> and normalizedUserKeybinds() claims Record<KeybindAction, string>, but both pass whatever keys happened to be in the stored JSON straight through. An old/typo action from a previous version of the app will be typed as KeybindAction to every caller, which is a small type lie that can hide bugs.

Filtering to KeybindAction members once at the boundary keeps the rest of the code honestly typed:

♻️ Suggested guard
   parsedUserKeybinds(): Partial<Record<KeybindAction, any>> {
     const raw = this.getString(KEYBINDS_KEY, "{}");
     try {
       const parsed = JSON.parse(raw);
       if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
-        return parsed;
+        const known = new Set<string>(Object.values(KeybindAction));
+        return Object.fromEntries(
+          Object.entries(parsed).filter(([k]) => known.has(k)),
+        ) as Partial<Record<KeybindAction, any>>;
       }
     } catch (e) {
       console.warn("Invalid keybinds JSON:", e);
     }
     return {};
   }

This also makes UserSettingModal.loadKeybindsFromStorage() safe without a duplicate guard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/UserSettings.ts` around lines 368 - 404, parsedUserKeybinds()
and normalizedUserKeybinds() currently accept any JSON keys and pretend they are
KeybindAction which can hide bad keys; update them to filter incoming entries to
only known KeybindAction members (use whatever canonical source exists for valid
actions — e.g., the KeybindAction enum/const array or a helper
isValidKeybindAction) before returning. Specifically, in parsedUserKeybinds()
validate/filter Object.keys(parsed) to only include keys that pass
isValidKeybindAction and return a Partial<Record<KeybindAction, any>> containing
only those entries; in normalizedUserKeybinds() operate on that filtered parsed
object and build Record<KeybindAction, string> by mapping only valid
KeybindAction keys and extracting string codes as you already do. Ensure you
reference parsedUserKeybinds and normalizedUserKeybinds and the KeybindAction
symbol when adding the guard so callers get correctly typed results.
tests/InputHandler.test.ts (1)

524-537: Reaching into UserSettings.cache from the test — fine here, but fragile.

(UserSettings as any).cache.delete(KEYBINDS_KEY) pokes a private static field. It works, but if the cache ever becomes per-instance or changes type, this silently breaks. Consider exposing a tiny test helper (e.g. a clearCache() in UserSettings) or reusing removeUserKeybinds(false) before the localStorage.setItem("not a json") line so the parse-failure path is what the test is really about.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/InputHandler.test.ts` around lines 524 - 537, The test directly mutates
the private static cache via (UserSettings as any).cache.delete(KEYBINDS_KEY)
which is fragile; change the test to clear existing keybinds through the public
API instead—either call the existing UserSettings.removeUserKeybinds(false)
before localStorage.setItem(KEYBINDS_KEY, "not a json") or add a small test-only
helper like UserSettings.clearCache() and call that; then run
inputHandler.initialize() and assert console.warn was called and (inputHandler
as any).keybinds.moveUp remains "KeyW". Ensure you reference KEYBINDS_KEY,
UserSettings.removeUserKeybinds (or new clearCache helper),
inputHandler.initialize, and keybinds.moveUp in the updated test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/InputHandler.ts`:
- Around line 389-392: The handler for the reset-graphics shortcut currently
hard-codes e.altKey but help text uses the configured emoji/menu modifier;
update the InputHandler so the reset branch uses the same configured-modifier
check as keybindMatchesEvent by calling a shared isMenuModifierPressed that
accepts both KeyboardEvent and PointerEvent (widen its signature to
KeyboardEvent | PointerEvent and update its logic to check the configured
modifier), remove the direct e.altKey check, and replace it with
isMenuModifierPressed(e) when handling this.keybinds.resetGfx (also apply the
same change to the other similar handlers mentioned around the other block).

In `@src/client/UserSettingModal.ts`:
- Around line 90-99: Remove the debug console.info call in UserSettingModal (the
line calling console.info inside the handleKeybindChange flow that logs
"handleKeybindChange recieved value...") so keybind changes no longer leak user
key data to the console; locate the block where let { action, value, key,
prevValue } = e.detail; is declared and delete or disable the console.info call
(or alternatively gate it behind an explicit debug flag) and keep the subsequent
normalization of key (key === "Dead" || key === "Unidentified" ? "" : key).

In `@src/client/Utils.ts`:
- Around line 389-394: The mapping has a typo: the key "Metat" should be "Meta",
so replace the incorrect property name Metat with Meta (keeping the value
Platform.isMac ? "⌘" : "⊞") so the platform-specific symbol is used instead of
falling back to a generic "Meta"; verify consistency with MetaLeft and MetaRight
entries and remove any accidental duplicate keys if present.

---

Outside diff comments:
In `@src/client/components/baseComponents/setting/SettingKeybind.ts`:
- Around line 95-106: Add a reserved-key guard in SettingKeybind's keydown
handler so Enter and arrow keys (ArrowUp, ArrowDown, ArrowLeft, ArrowRight,
"Enter") are treated like Tab/Escape: don't accept them as binds and cancel
listening when appropriate (use the existing this.listening flow and
requestUpdate()), or reference a typed reserved-key enum in
UserSettings/InputHandler if one exists; also remove the debug
console.log("Keydown event:", e) line. Ensure the handler prevents saving these
reserved keys while still allowing other keys to be bound, and keep
compatibility with InputHandler's existing ghost-confirm and camera movement
behavior.

In `@src/client/graphics/layers/UnitDisplay.ts`:
- Around line 129-219: The hotkey string in renderUnitItem is being manually
transformed with .replace(...).toUpperCase(), which misformats complex keys;
replace that logic by using the shared formatter function
formatKeyForDisplay(hotkey) (or the existing displayKey helper used elsewhere)
to compute displayHotkey so unit hotkeys match the labels shown in settings/help
and handle combos like Shift+BracketLeft correctly; update references in
renderUnitItem where displayHotkey is computed and used to call
formatKeyForDisplay instead of the chained replace/toUpperCase.

In `@src/client/InputHandler.ts`:
- Around line 222-276: The movement/zoom loop (moveInterval) currently returns
early whenever ShiftLeft/ShiftRight are down, which prevents Shift+key binds
from working; remove that early-return and change the key checks to be
Shift-aware by checking both the raw code and a Shift-prefixed variant when
Shift is held (e.g. if Shift active treat a bind like "W" as matching either
"KeyW" or "Shift+KeyW"); update the comparisons that reference this.activeKeys,
this.keybinds.moveUp/moveDown/moveLeft/moveRight and
this.keybinds.zoomIn/zoomOut so they test both the base code and the Shift+code
(or detect bind strings that include "Shift+"), then emit DragEvent/ZoomEvent as
before using PAN_SPEED/ZOOM_SPEED and eventBus.emit.

---

Nitpick comments:
In `@src/client/UserSettingModal.ts`:
- Around line 100-113: The loop variable shadows the outer action (from
e.detail), so rename the loop binding in the for-of that iterates
Object.entries(this.userKeybinds) (currently "for (const [action, codeAndKey]
...)") to a non-conflicting name like "keybindAction" or "userAction" and update
its usages inside the loop (and any related references such as codeAndKey and
normalizedCode) so the outer action remains unshadowed; ensure this change is
made where activeKeybinds is constructed and where codes is computed so behavior
is unchanged.
- Around line 51-79: During validation in the loop over Object.entries(parsed)
you currently assign validated[action] directly which preserves unknown/typo
keys; change the loop so you first verify that action is a known KeybindAction
(e.g. check it exists in the KeybindAction enum or in your canonical keybind map
like defaultKeybinds) and only then populate validated[action]; discard any
entries where the action is not recognized so userKeybinds only contains valid
KeybindAction keys.

In `@src/client/Utils.ts`:
- Around line 333-335: Remove the temporary debug logging from
formatKeyForDisplay: delete the console.info("formatKeyForDisplay recieved: " +
value); line (and the "TODO remove after testing" comment) in the
formatKeyForDisplay function in Utils.ts so the function no longer spams the
console during normal UI rendering; leave the function body to perform its
intended formatting and return the formatted string.

In `@src/client/vite-env.d.ts`:
- Around line 41-44: The Navigator.keyboard.getLayoutMap() signature currently
returns Promise<Map<string, string>> but the browser API is read-only; change
the return type to Promise<ReadonlyMap<string, string>> so callers can't assume
mutating methods (set/delete/clear) exist. Update the declaration for the
Navigator interface's keyboard property and its getLayoutMap() method to use
ReadonlyMap<string, string> (and import/ensure ReadonlyMap is available in the
ambient types) so the type matches KeyboardLayoutMap.

In `@src/core/game/UserSettings.ts`:
- Around line 4-10: Fix the typos and clarify the reserved-keys comment (change
"Shift+left quick" → "Shift+left click", "kindbinds" → "keybinds", "alltogether"
→ "altogether", and tidy phrasing about browser combos/reservations) and add a
short JSDoc above the exported KeyUnbound constant (export const KeyUnbound =
"Null") explaining that this is a JSON/string sentinel used to mark an
explicitly unbound action (do NOT use null, it is intentionally the string
"Null" and is compared with ===), so future readers aren’t confused by the
capitalized "Null".
- Around line 406-421: The loop in keybinds(isMac) uses for...in which yields
string-typed keys and loses the KeybindAction typing; replace that loop with
Object.entries(mergedKeybinds) to iterate [action, key] pairs, type action as
KeybindAction (or cast each entry to [KeybindAction, string]), and remove
entries where key === KeyUnbound (or build a new Record by filtering entries) so
mergedKeybinds retains proper typing and the same unbind behavior; refer to
functions getDefaultKeybinds, normalizedUserKeybinds, and the constant
KeyUnbound to locate the code to change.
- Around line 368-404: parsedUserKeybinds() and normalizedUserKeybinds()
currently accept any JSON keys and pretend they are KeybindAction which can hide
bad keys; update them to filter incoming entries to only known KeybindAction
members (use whatever canonical source exists for valid actions — e.g., the
KeybindAction enum/const array or a helper isValidKeybindAction) before
returning. Specifically, in parsedUserKeybinds() validate/filter
Object.keys(parsed) to only include keys that pass isValidKeybindAction and
return a Partial<Record<KeybindAction, any>> containing only those entries; in
normalizedUserKeybinds() operate on that filtered parsed object and build
Record<KeybindAction, string> by mapping only valid KeybindAction keys and
extracting string codes as you already do. Ensure you reference
parsedUserKeybinds and normalizedUserKeybinds and the KeybindAction symbol when
adding the guard so callers get correctly typed results.

In `@tests/InputHandler.test.ts`:
- Around line 524-537: The test directly mutates the private static cache via
(UserSettings as any).cache.delete(KEYBINDS_KEY) which is fragile; change the
test to clear existing keybinds through the public API instead—either call the
existing UserSettings.removeUserKeybinds(false) before
localStorage.setItem(KEYBINDS_KEY, "not a json") or add a small test-only helper
like UserSettings.clearCache() and call that; then run inputHandler.initialize()
and assert console.warn was called and (inputHandler as any).keybinds.moveUp
remains "KeyW". Ensure you reference KEYBINDS_KEY,
UserSettings.removeUserKeybinds (or new clearCache helper),
inputHandler.initialize, and keybinds.moveUp in the updated test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a158aa08-8a5a-4471-a371-bf0d29349e3d

📥 Commits

Reviewing files that changed from the base of the PR and between 0801798 and f3d7a31.

📒 Files selected for processing (11)
  • resources/lang/en.json
  • src/client/HelpModal.ts
  • src/client/InputHandler.ts
  • src/client/Main.ts
  • src/client/UserSettingModal.ts
  • src/client/Utils.ts
  • src/client/components/baseComponents/setting/SettingKeybind.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/vite-env.d.ts
  • src/core/game/UserSettings.ts
  • tests/InputHandler.test.ts

Comment thread src/client/InputHandler.ts
Comment thread src/client/UserSettingModal.ts Outdated
Comment thread src/client/Utils.ts
@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Apr 21, 2026
@openfrontio openfrontio Bot had a problem deploying to staging April 21, 2026 22:47 Failure
@VariableVince
Copy link
Copy Markdown
Contributor Author

@CodeRabbit pause reviews

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/InputHandler.test.ts (1)

874-876: ⚠️ Potential issue | 🔴 Critical

File does not parse: the outer describe("InputHandler AutoUpgrade") is never closed.

The Shift+ describe closes at line 874, but the outer AutoUpgrade describe opened on line 45 has no matching }); before the new top-level describe("Warship box selection (Shift+drag)") on line 876. That is exactly what Prettier is complaining about ('}' expected. at 1059:1). Add the missing }); between the two blocks.

🛠️ Proposed fix
       expect(uiState.ghostStructure).toBe(UnitType.City);
     });
   });
+});

 describe("Warship box selection (Shift+drag)", () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/InputHandler.test.ts` around lines 874 - 876, The test file is missing
the closing of the outer describe("InputHandler AutoUpgrade") block, causing a
parse error; add the missing closing sequence (});) immediately before the new
top-level describe("Warship box selection (Shift+drag)") declaration so that the
InputHandler AutoUpgrade describe is properly terminated and the subsequent
describe can start at top level.
src/client/UserSettingModal.ts (1)

682-700: ⚠️ Potential issue | 🔴 Critical

requestAlliance / breakAlliance entries were missed by the enum migration.

Every other <setting-keybind> now passes a KeybindAction.* value and this.defaultKeybinds.*, but these two still use raw strings and a hard-coded defaultKey="KeyK"/"KeyL". Two concrete problems:

  • breakAlliance is already in the enum, so it should be KeybindAction.breakAlliance with this.defaultKeybinds.breakAlliance for consistency.
  • requestAlliance is referenced by getDefaultKeybinds in UserSettings.ts ([KeybindAction.requestAlliance]: "KeyK") but is not declared in the KeybindAction enum — that file will not compile. Decide whether requestAlliance is an action or not, add it to the enum if yes, and switch this modal row to the enum + default.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/UserSettingModal.ts` around lines 682 - 700, The two
setting-keybind rows use raw strings and hard-coded defaultKey values; update
them to use the KeybindAction enum and the defaultKeybinds map: replace
action="requestAlliance"/"breakAlliance" and defaultKey="KeyK"/"KeyL" with
action={KeybindAction.requestAlliance|breakAlliance} and .value/.display sourced
from this.defaultKeybinds (i.e. .value=${this.defaultKeybinds.requestAlliance} /
.value=${this.defaultKeybinds.breakAlliance}), and ensure KeybindAction includes
requestAlliance (add KeybindAction.requestAlliance to the enum if missing to
match getDefaultKeybinds in UserSettings.ts) so the file compiles and the modal
is consistent with other keybind entries.
🧹 Nitpick comments (2)
src/client/UserSettingModal.ts (1)

633-636: Small polish on the attackRatioUp display binding.

Two tiny nits flagged by ESLint and the type system:

  • KeybindAction.attackRatioUp as KeybindAction — the cast is redundant; attackRatioUp is already a KeybindAction.
  • || "" should be ?? "" to avoid swallowing legitimately-empty strings (ESLint rule).

Also worth asking: why is only attackRatioUp wrapped in formatKeyForDisplay, while every other row relies on this.getKeyChar(...)? If this is intentional, a quick inline comment would help; if not, the rows are now inconsistent.

✨ Suggested change
-        .display=${formatKeyForDisplay(
-          this.getKeyValue(KeybindAction.attackRatioUp as KeybindAction) || "",
-        )}
+        .display=${formatKeyForDisplay(
+          this.getKeyValue(KeybindAction.attackRatioUp) ?? "",
+        )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/UserSettingModal.ts` around lines 633 - 636, The display binding
for attackRatioUp should remove the redundant cast and use the nullish
coalescing operator; update the call using
getKeyValue(KeybindAction.attackRatioUp) (no "as KeybindAction") and replace "||
''" with "?? ''". Also make the row consistent with other rows: either wrap it
with formatKeyForDisplay intentionally and add a brief inline comment next to
formatKeyForDisplay to explain why it differs, or switch to using
this.getKeyChar(this.getKeyValue(KeybindAction.attackRatioUp) ?? "") like the
others (adjust whichever matches the surrounding rows in UserSettingModal.ts).
src/core/game/UserSettings.ts (1)

387-427: Return types claim "full" records but the maps can be partial.

Both normalizedUserKeybinds() (line 388) and keybinds(isMac) (line 412) are typed as Record<KeybindAction, string>, but:

  • normalizedUserKeybinds only surfaces entries the user has customized — every other action is missing.
  • keybinds(isMac) actively deletes entries whose value equals KeyUnbound.

Downstream consumers (e.g. HelpModal doing keybinds.buildMenuModifier) therefore get undefined at runtime while the types promise string. Prefer Partial<Record<KeybindAction, string>> so callers must handle the unbound case explicitly.

A secondary nit: setUserKeybinds(value: Record<string, any>) at line 429 throws away the type information the callers already have. Tightening it to Partial<Record<KeybindAction, { value: string; key: string } | string>> (or similar) would keep the API honest with the rest of the refactor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/UserSettings.ts` around lines 387 - 427, Change the declared
return types from Record<KeybindAction, string> to Partial<Record<KeybindAction,
string>> for normalizedUserKeybinds() and keybinds(isMac) so the signatures
reflect that some actions may be missing/unbound; update any related local
typings if needed (e.g., the mergedKeybinds variable) so deletes and absent
entries are allowed. Also tighten the setUserKeybinds(...) parameter type from
Record<string, any> to something like Partial<Record<KeybindAction, { value:
string; key: string } | string>> so callers' types are preserved and match the
parsing logic used by normalizedUserKeybinds(). Ensure all referenced symbols
(normalizedUserKeybinds, keybinds, setUserKeybinds, KeyUnbound) compile after
the change and update any call sites that assumed a full Record to handle
possibly undefined values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/game/UserSettings.ts`:
- Around line 12-47: The KeybindAction enum contains a duplicated member
swapDirection causing a redeclaration error and is missing requestAlliance
referenced by getDefaultKeybinds and UserSettingModal.ts; fix by removing the
duplicate swapDirection entry from the KeybindAction enum and add a new member
requestAlliance = "requestAlliance" to the same enum so getDefaultKeybinds
(which uses [KeybindAction.requestAlliance]) and UserSettingModal can reference
the enum member instead of the raw string; ensure no other duplicates were
introduced when editing the enum.

---

Outside diff comments:
In `@src/client/UserSettingModal.ts`:
- Around line 682-700: The two setting-keybind rows use raw strings and
hard-coded defaultKey values; update them to use the KeybindAction enum and the
defaultKeybinds map: replace action="requestAlliance"/"breakAlliance" and
defaultKey="KeyK"/"KeyL" with
action={KeybindAction.requestAlliance|breakAlliance} and .value/.display sourced
from this.defaultKeybinds (i.e. .value=${this.defaultKeybinds.requestAlliance} /
.value=${this.defaultKeybinds.breakAlliance}), and ensure KeybindAction includes
requestAlliance (add KeybindAction.requestAlliance to the enum if missing to
match getDefaultKeybinds in UserSettings.ts) so the file compiles and the modal
is consistent with other keybind entries.

In `@tests/InputHandler.test.ts`:
- Around line 874-876: The test file is missing the closing of the outer
describe("InputHandler AutoUpgrade") block, causing a parse error; add the
missing closing sequence (});) immediately before the new top-level
describe("Warship box selection (Shift+drag)") declaration so that the
InputHandler AutoUpgrade describe is properly terminated and the subsequent
describe can start at top level.

---

Nitpick comments:
In `@src/client/UserSettingModal.ts`:
- Around line 633-636: The display binding for attackRatioUp should remove the
redundant cast and use the nullish coalescing operator; update the call using
getKeyValue(KeybindAction.attackRatioUp) (no "as KeybindAction") and replace "||
''" with "?? ''". Also make the row consistent with other rows: either wrap it
with formatKeyForDisplay intentionally and add a brief inline comment next to
formatKeyForDisplay to explain why it differs, or switch to using
this.getKeyChar(this.getKeyValue(KeybindAction.attackRatioUp) ?? "") like the
others (adjust whichever matches the surrounding rows in UserSettingModal.ts).

In `@src/core/game/UserSettings.ts`:
- Around line 387-427: Change the declared return types from
Record<KeybindAction, string> to Partial<Record<KeybindAction, string>> for
normalizedUserKeybinds() and keybinds(isMac) so the signatures reflect that some
actions may be missing/unbound; update any related local typings if needed
(e.g., the mergedKeybinds variable) so deletes and absent entries are allowed.
Also tighten the setUserKeybinds(...) parameter type from Record<string, any> to
something like Partial<Record<KeybindAction, { value: string; key: string } |
string>> so callers' types are preserved and match the parsing logic used by
normalizedUserKeybinds(). Ensure all referenced symbols (normalizedUserKeybinds,
keybinds, setUserKeybinds, KeyUnbound) compile after the change and update any
call sites that assumed a full Record to handle possibly undefined values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 762ddfbc-f358-4248-aab9-f649855316fe

📥 Commits

Reviewing files that changed from the base of the PR and between f3d7a31 and cda0306.

📒 Files selected for processing (8)
  • resources/lang/en.json
  • src/client/HelpModal.ts
  • src/client/InputHandler.ts
  • src/client/Main.ts
  • src/client/UserSettingModal.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/core/game/UserSettings.ts
  • tests/InputHandler.test.ts
✅ Files skipped from review due to trivial changes (3)
  • src/client/Main.ts
  • resources/lang/en.json
  • src/client/InputHandler.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/graphics/layers/UnitDisplay.ts

Comment thread src/core/game/UserSettings.ts
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

@VariableVince: Sure! I'll pause automatic reviews for this PR.


🧠 Learnings used
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1655
File: resources/maps/giantworldmap/manifest.json:165-174
Timestamp: 2025-07-31T12:03:08.052Z
Learning: In OpenFrontIO draft PRs, flag names in map manifest files may be placeholders that will be updated to match the actual SVG flag file names when the assets are received. The final naming depends on the actual flag SVG files provided, not code naming conventions.
✅ Actions performed

Reviews paused.

@openfrontio openfrontio Bot had a problem deploying to staging April 30, 2026 22:51 Failure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bugfix Fixes a bug Refactor Code cleanup, technical debt, refactoring, and architecture improvements.

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

1 participant