Skip to content

Improve input mappings with conflict handling and unbound warnings - #409

Merged
ikostan merged 79 commits into
mainfrom
enable-keyboard-&-dpad-navigation-for-key-mapping-settings
Feb 18, 2026
Merged

Improve input mappings with conflict handling and unbound warnings#409
ikostan merged 79 commits into
mainfrom
enable-keyboard-&-dpad-navigation-for-key-mapping-settings

Conversation

@ikostan

@ikostan ikostan commented Feb 16, 2026

Copy link
Copy Markdown
Owner

name: Default Pull Request Template
about: Suggesting changes to SkyLockAssault
title: ''
labels: ''
assignees: ''

image image image image image

Description

What does this PR do? (e.g., "Fixes player jump physics in level 2" or "Adds
new enemy AI script")

Related Issue

Closes #ISSUE_NUMBER (if applicable)

Changes

  • List key changes here (e.g., "Updated Jump.gd to use Godot 4.4's new Tween
    system")
  • Any breaking changes? (e.g., "Deprecated old signal; migrate to new one")

Testing

  • Ran the game in Godot v4.5 editor—describe what you tested (e.g., "Jump
    works on Win10 with 60 FPS")
  • Any new unit tests added? (Link to test scene if yes)
  • Screenshots/GIFs if UI-related: (Attach below)

Checklist

  • Code follows Godot style guide (e.g., snake_case for variables)
  • No console errors in editor/output
  • Ready for review!

Additional Notes

Anything else? (e.g., "Tested on Win10 64-bit; needs Linux validation")

Summary by Sourcery

Enhancements:

  • Add a post-load check that backfills missing default keyboard and gamepad mappings into the InputMap and persists them to settings when needed.

Summary by Sourcery

Improve input mapping robustness, conflict handling, and device-aware UI/UX across settings, key mapping, main menu, and gameplay while expanding automated tests and documentation.

New Features:

  • Add conflict detection and confirmation dialog when assigning inputs already used by other actions, with device-specific unbinding logic.
  • Introduce per-device tracking and persistence of the last selected input device to drive labels, prompts, and unbound checks.
  • Display in-game warnings for unbound critical controls or presses of completely unbound inputs, guiding players to the key mapping menu.
  • Provide helper APIs for querying pause binding labels, event bindings, conflicts, and event device types from settings.
  • Add a utility to load the Key Mapping menu directly from other menus while preserving the background video and menu stack.
  • Extend main scene messaging system to show temporary HUD messages with typed flags for different warning types.

Bug Fixes:

  • Ensure default input mappings are backfilled and saved robustly even with corrupted or legacy config formats, including proper handling of explicit unbound states.
  • Fix migration logic to use a generic save-needed flag instead of an old migration-only flag and make legacy unbound critical actions migrated once and persisted.
  • Prevent gamepad defaults or resets from creating duplicate or cross-action bindings, fully clearing device-specific events before restoring defaults.
  • Avoid crashes and misbehavior when loading malformed input arrays by ignoring non-string entries and treating invalid data safely.
  • Make loading screen progress rely on real threaded loading progress (with web-specific timing) to avoid the progress bar getting stuck or desynced.
  • Ensure focus restoration when exiting the key mapping menu returns to the appropriate button in options or main menu, improving keyboard/DPAD navigation.
  • Fix unbound critical-controls warning so it is device-aware and shown only once per session until bindings are actually fixed.

Enhancements:

  • Refactor input default backfilling into a shared helper for reuse in settings, migrations, and tests.
  • Tighten reset-to-default behavior to log and fully reset per device while keeping other device bindings intact.
  • Refine remap button prompts to be device-specific and adjust remap flow to support conflict-aware behavior and better logging.
  • Improve key mapping menu focus logic so keyboard/gamepad toggles, remap buttons, and back/reset controls are reachable via keyboard/DPAD from first open.
  • Expand tests for settings, input remapping, key mapping UI, and combined config loading to cover new conflict, unbound, migration, and device-state behaviors.
  • Clarify and slightly reorganize global utility scripts (globals.gd, loading_screen.gd, main_scene.gd) with better comments and small structural improvements.

Documentation:

  • Add a deployment guide detailing free web browser game platforms and recommended automation strategies for Godot Web exports.
  • Update README to reference the new web deployment platforms guide.

Tests:

  • Add new edge-case tests for settings to cover conflict-unbound persistence, last-device validation, legacy migration, and corrupt config recovery.
  • Extend key mapping menu tests to cover improved device-aware labels, reset behavior, persistence, and default backfill for keyboard and gamepad.
  • Update input remap button tests to validate device-specific prompts, direct remap behavior, and no-input cancellation after UI changes.
  • Adjust existing system tests to align with the new save flag semantics and per-device default behavior for inputs.

Chores:

  • Tidy comments and headers across multiple scripts to better document responsibilities and link to external test plans.

Summary by Sourcery

Improve input handling, device awareness, and user feedback for key mappings and gameplay while hardening settings migration and loading behavior.

New Features:

  • Add conflict-aware input remapping with a confirmation dialog that unbinds only conflicting device-specific bindings.
  • Introduce device-aware pause binding labels and last-used input device persistence to drive UI labels and warnings.
  • Display in-game HUD messages for critical unbound controls and presses of entirely unbound inputs, guiding players toward the key mapping menu.
  • Provide global helper to open the Key Mapping menu directly from other menus while keeping the background video playing.

Bug Fixes:

  • Ensure default input mappings are backfilled and saved even when configs are missing, corrupted, or in legacy formats, including explicit unbound states.
  • Prevent reset-to-default and gamepad defaults from creating duplicate or cross-action bindings by fully clearing per-device events before restoring defaults.
  • Fix unbound critical-controls warning so it is device-aware, shown only once per session, and cleared when bindings are fixed.
  • Avoid crashes when loading malformed input arrays by safely ignoring non-string entries and using robust type handling.
  • Make the loading screen progress rely on actual threaded loading progress with web-specific timing to avoid stuck or misleading progress bars.
  • Restore focus correctly when exiting the key mapping menu back to options or main menu to improve keyboard/controller navigation.

Enhancements:

  • Refactor input default backfill and legacy migration into reusable helpers that respect explicit unbound states and per-device bindings.
  • Add helpers for detecting conflicting actions, checking if events are bound, and determining event device type to support richer input logic.
  • Improve key mapping menu focus behavior, device toggle handling, and remap prompts so keyboard and gamepad flows are clearer and more accessible.
  • Centralize temporary HUD messaging with typed flags in the main scene to avoid repeated unbound warnings and manage message lifetimes cleanly.
  • Switch global remap prompt text to device-specific variants to avoid layout shifts and clarify what input is expected.
  • Clarify and slightly reorganize global utility scripts, loading screen logic, and options menu focus handling for better structure and consistency.

Documentation:

  • Add a guide to free web browser deployment platforms for Godot Web exports and link it from the README.

Tests:

  • Expand settings, input remapping, and key mapping menu tests to cover conflict handling, explicit unbound persistence, device-aware labels, and corrupt config recovery.
  • Extend combined-config and error-edge-case tests to align with the new save flag semantics, legacy migration, and per-device default behavior.
  • Add regression tests for pause binding labels, device validation, and HUD messages to ensure new input-label helpers behave as expected.

Chores:

  • Tighten comments and headers across scripts and tests to better document responsibilities and reference external test plans.

Summary by CodeRabbit

  • New Features

    • Conflict-resolution dialog for control remapping, device-specific remap prompts, remember last input device, HUD messages for unbound inputs, Start Game confirmation when critical controls are unbound, and key-mapping submenu loader.
  • Bug Fixes

    • Safer migration/backfill of input defaults, robust handling of corrupted legacy configs, fixes for duplicate bindings on reset, and reliable threaded loading progress.
  • Improvements

    • Redesigned key-mapping navigation and focus, updated visuals and larger buttons, device-aware remap flow, improved back-navigation behavior.
  • Documentation

    • Added guide for free web deployment platforms (CI/CD and export tips).
  • Tests

    • Expanded and updated tests for remapping, conflict flows, device-aware prompts, and async UI sequencing.

…ing any labels #408

Root Cause: _ready() + load_input_mappings() logic does not guarantee defaults exist on first run in HTML5 export. Some actions (menu keys) remain empty in InputMap.

Effect: UI shows blank key labels on first load. Fixed after save because save_input_mappings() writes them.

Solution: Add _ensure_defaults_saved() after load and save if any defaults were added. This ensures the UI always has proper InputMap entries, even on web first-run.
[FEATURE] Enable D-Pad Navigation for Key Mapping Settings #407
[FEATURE] Enable Keyboard Navigation for Key Mapping Settings #406
@ikostan ikostan added this to the Milestone 12: TBD milestone Feb 16, 2026
@ikostan ikostan self-assigned this Feb 16, 2026
@ikostan ikostan added the bug Something isn't working label Feb 16, 2026
@ikostan ikostan linked an issue Feb 16, 2026 that may be closed by this pull request
@ikostan ikostan added enhancement New feature or request menu GUI labels Feb 16, 2026
@sourcery-ai

sourcery-ai Bot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors and hardens input mapping, conflict handling, and device-aware UI flows across settings, key mapping, main menu, and gameplay, while adding helper APIs, HUD warnings, and expanded tests and docs to support robust per-device bindings and web deployment.

Sequence diagram for conflict-aware input remapping flow

sequenceDiagram
    actor Player
    participant InputRemapButton
    participant KeyMappingMenu
    participant Settings
    participant InputMap
    participant MainScene

    Player->>InputRemapButton: press button
    InputRemapButton->>InputRemapButton: _on_pressed()
    InputRemapButton->>InputRemapButton: listening = true

    Player->>InputRemapButton: input event (key/button/axis)
    InputRemapButton->>InputRemapButton: _input(event)
    InputRemapButton->>InputRemapButton: build new_event
    InputRemapButton->>Settings: get_conflicting_actions(new_event, action)
    Settings-->>InputRemapButton: conflicts Array

    alt conflicts not empty and not same as existing binding
        InputRemapButton->>KeyMappingMenu: get_first_node_in_group(key_mapping_menu)
        InputRemapButton->>KeyMappingMenu: show_conflict_dialog(self, new_event, conflicts)
        KeyMappingMenu->>KeyMappingMenu: store current_remap_button, current_pending_event, current_conflicts
        KeyMappingMenu->>KeyMappingMenu: conflict_dialog.popup_centered()

        alt player confirms reassign
            Player->>KeyMappingMenu: confirm conflict_dialog
            KeyMappingMenu->>KeyMappingMenu: _on_conflict_confirmed()
            loop for each conflicting action
                KeyMappingMenu->>Settings: events_match(ev, current_pending_event)
                Settings-->>KeyMappingMenu: bool
                KeyMappingMenu->>InputMap: action_erase_event(conflict_action, ev)
            end
            KeyMappingMenu->>InputRemapButton: erase_old_event()
            KeyMappingMenu->>InputMap: action_add_event(action, current_pending_event)
            KeyMappingMenu->>Settings: save_input_mappings()
            KeyMappingMenu->>InputRemapButton: finish_remap()
            KeyMappingMenu->>KeyMappingMenu: _clear_conflict_state()
            KeyMappingMenu->>KeyMappingMenu: update_all_remap_buttons()
            KeyMappingMenu->>MainScene: clear_unbound_warning()
        else player cancels
            Player->>KeyMappingMenu: cancel conflict_dialog
            KeyMappingMenu->>KeyMappingMenu: _on_conflict_canceled()
            KeyMappingMenu->>InputRemapButton: button_pressed = false
            KeyMappingMenu->>InputRemapButton: listening = false
            KeyMappingMenu->>InputRemapButton: update_button_text()
            KeyMappingMenu->>KeyMappingMenu: _clear_conflict_state()
        end
    else no conflicts
        InputRemapButton->>InputRemapButton: erase_old_event()
        InputRemapButton->>InputMap: action_add_event(action, new_event)
        InputRemapButton->>InputRemapButton: finish_remap()
    end
Loading

Sequence diagram for Start Game with unbound controls warning

sequenceDiagram
    actor Player
    participant MainMenu
    participant Settings
    participant Globals
    participant KeyMappingMenu
    participant LoadingScreen

    Player->>MainMenu: press Start button
    MainMenu->>MainMenu: _on_start_pressed()
    MainMenu->>Settings: has_unbound_critical_actions_for_current_device()
    Settings-->>MainMenu: bool

    alt critical actions unbound
        MainMenu->>MainMenu: start_button.disabled = true
        MainMenu->>MainMenu: unbound_dialog.popup_centered()

        alt player chooses Open Key Mapping
            Player->>MainMenu: confirm unbound_dialog
            MainMenu->>Globals: load_key_mapping(ui_panel)
            Globals->>ui_panel: visible = false
            Globals->>Globals: hidden_menus.push_back(ui_panel)
            Globals->>KeyMappingMenu: instantiate and add_child
            KeyMappingMenu-->>Player: key mapping menu visible
        else player chooses Start Anyway
            Player->>MainMenu: press cancel button
            MainMenu->>MainMenu: unbound_dialog.hide()
            MainMenu->>Globals: load_scene_with_loading(main_scene.tscn)
            Globals->>Globals: next_scene = main_scene.tscn
            Globals->>LoadingScreen: change_scene_to_file(loading_screen.tscn)
            LoadingScreen-->>Player: loading screen shows
        end

        MainMenu->>MainMenu: unbound_dialog.visibility_changed
        MainMenu->>MainMenu: if not visible
        MainMenu->>MainMenu: start_button.disabled = false
        MainMenu->>MainMenu: start_button.grab_focus()
    else no unbound critical actions
        MainMenu->>Globals: load_scene_with_loading(main_scene.tscn)
        Globals->>Globals: next_scene = main_scene.tscn
        Globals->>LoadingScreen: change_scene_to_file(loading_screen.tscn)
        LoadingScreen-->>Player: loading screen shows
    end
Loading

Sequence diagram for in-game unbound input HUD warnings

sequenceDiagram
    actor Player
    participant MainScene
    participant Settings

    Player->>MainScene: input event (key/button/axis)
    MainScene->>MainScene: _input(event)
    MainScene->>MainScene: ignore echo and small axis jitter

    alt event is significant and from key or gamepad
        MainScene->>Settings: is_event_bound(event)
        Settings-->>MainScene: bool
        alt event is unbound and no current unbound key message
            MainScene->>MainScene: _showing_unbound_key_message = true
            MainScene->>Settings: get_event_device_type(event)
            Settings-->>MainScene: device_type
            MainScene->>Settings: get_pause_binding_label_for_device(device_type)
            Settings-->>MainScene: pause_label
            MainScene->>MainScene: build HUD text with pause_label
            MainScene->>MainScene: show_message(text, MessageType.KEY_PRESS_UNBOUND)
            MainScene->>MainScene: message_label.visible = true
            MainScene->>MainScene: await timer 4.0 seconds
            MainScene->>MainScene: message_label.visible = false
            MainScene->>MainScene: _showing_unbound_key_message = false
        else event is bound or message already showing
            MainScene-->>Player: no HUD message change
        end
    else unsupported event type
        MainScene-->>Player: ignore
    end
Loading

Class diagram for updated input mapping and messaging system

classDiagram
    class Settings {
        <<Node>>
        +const LEGACY_MIGRATION_KEY String
        +const CONFIG_PATH String
        +const CRITICAL_ACTIONS Array~String~
        +const ACTIONS Array~String~
        +const DEFAULT_KEYBOARD Dictionary
        +const DEFAULT_GAMEPAD Dictionary
        -_needs_save bool
        +_ready() void
        +load_input_mappings(path String, actions Array~String~) void
        +save_input_mappings(path String, actions Array~String~) void
        +reset_to_defaults(device_type String) void
        +has_unbound_critical_actions() bool
        +has_unbound_critical_actions_for_current_device() bool
        +get_pause_binding_label() String
        +get_pause_binding_label_for_device(device_type String) String
        +is_event_bound(event InputEvent) bool
        +get_conflicting_actions(event InputEvent, exclude_action String) Array~String~
        +events_match(a InputEvent, b InputEvent) bool
        +save_last_input_device(device String) void
        +load_last_input_device() void
        +get_event_device_type(event InputEvent) String
        +get_event_label(ev InputEvent) String
        -_add_missing_defaults(config ConfigFile) bool
        -_migrate_legacy_unbound_states() void
        -_deserialize_and_add(action String, serialized String) void
    }

    class Globals {
        <<Node>>
        enum LogLevel
        +const REMAP_PROMPT_KEYBOARD String
        +const REMAP_PROMPT_GAMEPAD String
        +current_log_level LogLevel
        +enable_debug_logging bool
        +options_instance CanvasLayer
        +hidden_menus Array~Node~
        +options_open bool
        +key_mapping_scene PackedScene
        +previous_scene String
        +options_scene PackedScene
        +next_scene String
        +current_input_device String
        +_ready() void
        +log_message(msg String, level LogLevel) void
        +ensure_initial_focus(candidate Control, allowed_controls Array~Control~, context String) void
        +load_key_mapping(menu_to_hide Node) void
        +load_scene_with_loading(path String) void
        +_load_settings(path String) void
    }

    class KeyMappingMenu {
        <<CanvasLayer>>
        +js_window Variant
        +os_wrapper OSWrapper
        +js_bridge_wrapper JavaScriptBridgeWrapper
        +conflict_dialog ConfirmationDialog
        +current_remap_button InputRemapButton
        +current_pending_event InputEvent
        +current_conflicts Array~String~
        +_ready() void
        +show_conflict_dialog(btn InputRemapButton, new_event InputEvent, conflicts Array~String~) void
        +_on_conflict_confirmed() void
        +_on_conflict_canceled() void
        +_clear_conflict_state() void
        +update_all_remap_buttons() void
        +_on_reset_pressed() void
        +_on_controls_back_button_pressed() void
        +_on_controls_back_button_pressed_js(args Array) void
        +_grab_initial_focus() void
        +_on_keyboard_toggled(toggled_on bool) void
        +_on_gamepad_toggled(toggled_on bool) void
    }

    class InputRemapButton {
        <<Button>>
        +enum DeviceType
        +action String
        +current_device DeviceType
        +listening bool
        +AXIS_DEADZONE_THRESHOLD float
        +_ready() void
        +_on_pressed() void
        +_input(event InputEvent) void
        +erase_old_event() void
        +finish_remap() void
        +update_button_text() void
        +get_matching_event() InputEvent
        +get_normalized_axis_direction(value float) float
    }

    class MainScene {
        <<Node2D>>
        +enum MessageType
        -_showing_unbound_warning bool
        -_showing_unbound_key_message bool
        +player Node2D
        +stats_panel Panel
        +background ParallaxBackground
        +bushes_layer ParallaxLayer
        +decor_layer ParallaxLayer
        +texture_preloader ResourcePreloader
        +hud CanvasLayer
        +message_label Label
        +_ready() void
        +_input(event InputEvent) void
        +_process(delta float) void
        +show_message(text String, type MessageType) void
        +clear_unbound_warning() void
        +setup_bushes_layer(viewport Vector2) void
        +setup_decor_layer(viewport Vector2) void
    }

    class MainMenu {
        <<CanvasLayer>>
        +quit_dialog ConfirmationDialog
        +unbound_dialog ConfirmationDialog
        +options_menu PackedScene
        +last_focused_button Button
        +_ready() void
        +_input(event InputEvent) void
        +_setup_quit_dialog() void
        +_setup_unbound_dialog() void
        +_on_start_pressed(args Array) void
        +_on_options_button_pressed(args Array) void
        +_on_quit_pressed() void
    }

    class LoadingScreen {
        <<Control>>
        +progress_bar ProgressBar
        +status_label Label
        +load_failed bool
        +is_scene_loaded bool
        +loader_progress float
        +min_load_time float
        +load_start_time float
        +transitioning bool
        +_ready() void
        +_process(delta float) void
    }

    class OptionsMenu {
        <<CanvasLayer>>
        +advanced_settings_button Button
        +audio_settings_button Button
        +key_mapping_button Button
        +gameplay_settings_button Button
        +options_back_button Button
        +_ready() void
        +grab_focus_on_key_mapping_button() void
        +_grab_first_button_focus() void
    }

    Settings ..> ConfigFile
    Settings ..> InputEvent
    Settings ..> InputEventKey
    Settings ..> InputEventJoypadButton
    Settings ..> InputEventJoypadMotion
    Settings ..> Globals

    Globals ..> OptionsMenu
    Globals ..> KeyMappingMenu

    KeyMappingMenu o--> ConfirmationDialog
    KeyMappingMenu o--> InputRemapButton
    KeyMappingMenu ..> Globals
    KeyMappingMenu ..> Settings
    KeyMappingMenu ..> MainScene

    InputRemapButton ..> Settings
    InputRemapButton ..> Globals
    InputRemapButton ..> KeyMappingMenu

    MainScene ..> Settings
    MainScene ..> Globals

    MainMenu ..> Settings
    MainMenu ..> Globals

    LoadingScreen ..> Globals

    OptionsMenu ..> Globals
Loading

File-Level Changes

Change Details Files
Refactor settings input mapping to support per-device defaults, explicit unbound persistence, conflict queries, and last-device tracking.
  • Introduce CRITICAL_ACTIONS list and helper predicates for unbound checks, including device-aware variants.
  • Replace migration flag with a generic _needs_save flag, add _add_missing_defaults and _migrate_legacy_unbound_states helpers, and call them from load_input_mappings/_ready.
  • Harden config loading against malformed arrays/legacy formats and ensure defaults are backfilled and saved when needed, including legacy unbound handling.
  • Add APIs for event serialization helpers, events_match, get_conflicting_actions, event device-type detection, pause-binding label lookup, and last-input device save/load.
scripts/settings.gd
test/gut/test_settings_ec.gd
test/gut/test_combined_multi_manager_loads.gd
test/gut/test_error_edge_cases.gd
test/test_settings.gd
test/gut/test_get_pause_binding_label_for_device.gd
Enhance key mapping menu with conflict-aware remapping, device persistence, and improved focus/navigation.
  • Add a conflict ConfirmationDialog, state for current remap and conflicts, and handlers that unbind only conflicting same-device events before applying a new binding.
  • Wire InputRemapButton to call conflict detection via Settings.get_conflicting_actions, fall back safely if dialog missing, and use device-specific remap prompts.
  • Track and persist current_input_device via Globals/Settings, update remap button labels on toggles, restore last-selected device on open, and improve initial/focus behavior when opening and closing the menu.
  • Adjust key mapping tests to seed defaults, use conflict-free buttons for remaps, assert device-aware prompts/labels, and verify per-device reset and persistence semantics.
scripts/key_mapping.gd
scripts/input_remap_button.gd
scripts/globals.gd
scripts/options_menu.gd
test/gut/test_key_mapping_menu.gd
test/gut/test_key_mapping_menu_device_aware.gd
test/gut/test_input_remap_button.gd
test/gut/test_input_remap_ec.gd
scenes/key_mapping_menu.tscn
Add in-game and main-menu UX for unbound controls, including HUD messages and a start-game warning dialog.
  • Promote main_scene.gd to class_name MainScene, add HUD/message_label references, and implement show_message with typed flags and timers.
  • Detect completely unbound input presses in _input using Settings.is_event_bound and show a temporary HUD hint with device-aware pause-label text.
  • Show a one-time-per-session critical unbound-controls HUD warning in _process using Settings.has_unbound_critical_actions_for_current_device and clear flag when bindings are fixed.
  • Extend main_menu.gd with an "unbound controls" ConfirmationDialog shown from _on_start_pressed, integrating with Globals.load_key_mapping and load_scene_with_loading, and guarding Start button state/focus restoration.
scripts/main_scene.gd
scripts/main_menu.gd
scripts/settings.gd
scripts/key_mapping.gd
scripts/globals.gd
scenes/main_scene.tscn
scenes/pause_menu.tscn
Improve loading screen behavior and documentation for web deployment and async flows.
  • Update loading_screen.gd to use ResourceLoader.load_threaded_request with sub-threads, rely solely on real progress, bump min_load_time for web, and remove fake progress logic.
  • Tighten combined-manager tests to reflect new per-device default semantics and _needs_save flag usage.
  • Add a detailed Platforms_for_Web_Deployment_Guide.md and link it from the README for free browser deployment and CI/CD guidance.
scripts/loading_screen.gd
test/gut/test_combined_multi_manager_loads.gd
README.md
files/docs/Platforms_for_Web_Deployment_Guide.md

Assessment against linked issues

Issue Objective Addressed Explanation
#406 Enable full keyboard (and D-pad) focus/navigation for all interactive controls in the Key Mapping Settings menu, including making them focusable, defining a logical focus order, and setting an appropriate initial focus when the menu opens.
#406 Ensure the Key Mapping menu is operable without a mouse for selecting actions, triggering rebinding, switching device sections, and returning to previous menus, while maintaining stable, visible focus and restoring focus appropriately when exiting back to Options/Main Menu.
#407 Make the Key Mapping Settings menu fully navigable via gamepad D-Pad, including mapping D-Pad to ui_up/ui_down/ui_left/ui_right and ensuring an initial valid focus when the menu opens.
#407 Ensure all interactive elements in the key_mapping_menu (device toggles, remap buttons, reset/back buttons) participate correctly in Godot’s focus system so that controller users can move vertically and horizontally through the UI with D-Pad and maintain clear visual focus, including focus restoration when leaving the menu.
#407 Ensure the input rebinding flow is controller-friendly: D-Pad navigation does not interfere with remapping, conflict handling and saving are robust, and behavior is validated by automated tests without breaking mouse/keyboard interaction.
#408 Ensure that on first run (including Web exports with no existing settings.cfg), all keyboard default mappings for both player controls and menu/UI actions (e.g., ui_accept, ui_up/down/left/right, pause) are correctly backfilled into the InputMap and thus show as labeled bindings in the Key Mapping menu.
#410 Add conflict detection when remapping an action to an input that is already bound to other actions, and show a warning/confirmation dialog in the key mapping UI.
#410 When a conflict is detected, on confirm (reassign) unbind the conflicting action(s) and assign the new binding, and on cancel restore/keep the original binding with no change.
#411 Add settings-side support for detecting unbound critical actions (defining the critical actions list and exposing an API to check if any are unbound).
#411 Update the Start Game flow to warn the player when starting if any critical controls are unbound, via a confirmation dialog that offers opening the Key Mapping menu or starting the game anyway.
#412 Detect during gameplay when any critical control action (e.g., fire, movement, pause) is unbound, using settings helpers that can be queried from gameplay code.
#412 When a critical action is unbound during gameplay, show a non-intrusive HUD/in-game message guiding the player to key mapping (without auto-pausing the game or forcing a menu), and also warn when the player presses completely unbound inputs.
#415 Update the loading screen logic so that on Web exports the progress bar no longer stalls at 50%, by relying on real threaded ResourceLoader progress (with sub-threads enabled), adding appropriate Web-specific minimum load time, and keeping smooth/accurate progress display including failure fallback.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

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

Walkthrough

Device-aware key remapping, conflict-resolution UI, and extensive focus/navigation wiring for the Key Mapping menu were added; Settings gained device helpers, migration/backfill and critical-action checks; HUD transient messaging and threaded loading improvements were introduced; docs and multiple tests were updated accordingly.

Changes

Cohort / File(s) Summary
Key Mapping Scene & UI
scenes/key_mapping_menu.tscn
Replaced many StyleBoxEmpty with StyleBoxFlat (bg_color, corner radii, shadow); widened panel offsets; extensive focus_neighbor_*, focus_next, focus_previous, and focus-style updates across toggles, remap inputs, Back/Reset, and menu controls.
Settings & InputMap defaults
scripts/settings.gd
Added LEGACY_MIGRATION_KEY, CRITICAL_ACTIONS, replaced _needs_migration with _needs_save, implemented backfill _add_missing_defaults, migration _migrate_legacy_unbound_states, conflict helpers (_events_match, get_conflicting_actions), device helpers (last-device save/load, device type, pause binding label), and improved reset/persistence logic.
Input Remap Button & Globals
scripts/input_remap_button.gd, scripts/globals.gd
Device-specific remap prompts (REMAP_PROMPT_KEYBOARD/REMAP_PROMPT_GAMEPAD), device-filtered remap input, axis normalization, conflict detection and dialog routing; Globals adds key_mapping_scene, current_input_device, load_key_mapping(), and _save_settings() plus difficulty export.
Key Mapping Logic & Conflict Resolution
scripts/key_mapping.gd
Adds in-UI conflict dialog and state, show_conflict_dialog() and confirm/cancel handlers, _clear_conflict_state(), _grab_initial_focus(), last-device initialization, and refined focus restoration on close.
Options / Main / HUD changes
scripts/options_menu.gd, scripts/main_menu.gd, scenes/main_scene.tscn, scripts/main_scene.gd
Added grab_focus_on_key_mapping_button(); Start Game now checks has_unbound_critical_actions() and may prompt to open Key Mapping; new HUD CanvasLayer + MessageLabel; main scene shows transient messages for unbound inputs and detects unbound input events.
Loading / Pause tweaks
scripts/loading_screen.gd, scenes/pause_menu.tscn
Loading uses threaded real progress with web breathing-room and sub-thread flags; removed fake progress logic; pause Panel set top_level = true.
Tests updated
test/gut/*, test/*
Multiple GUT updates to reflect device-aware prompts, async frame awaits, renamed migration flag _needs_save, updated persistence/reset expectations, and added conflict/unbind persistence and legacy-migration tests.
Docs
README.md, files/docs/Platforms_for_Web_Deployment_Guide.md
Added README entry and a new guide on zero-cost Godot v4.5 HTML5/WebGL deployment platforms with CI/Playwright automation tips and sample workflows.

Sequence Diagram(s)

sequenceDiagram
    participant Player as Player
    participant IRB as InputRemapButton
    participant KMM as KeyMappingMenu
    participant Settings as Settings
    participant Dialog as ConflictDialog

    Player->>IRB: press to start remap
    IRB->>IRB: show device-specific prompt
    Player-->>IRB: input event (key / joy / axis)
    IRB->>Settings: get_conflicting_actions(new_event)
    alt conflicts found
        IRB->>KMM: request show_conflict_dialog(btn,new_event,conflicts)
        KMM->>Dialog: display conflicts and options
        alt user confirms
            KMM->>Settings: unbind_conflicting_actions()
            KMM->>Settings: add_new_binding(new_event)
            KMM->>IRB: finish_remap(success)
        else user cancels
            KMM->>IRB: cancel_remap()
        end
    else no conflicts
        IRB->>Settings: erase_old_event(), add_new_binding(new_event)
        IRB->>IRB: finish_remap(success)
    end
    Note right of Settings: may persist InputMap and save last-input-device
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels

testing

Poem

🐇
I hopped through menus, nudged a stray bind,
I widened focus paths so inputs align.
I asked, "Override?" — unbound keys made right,
Now players play from morning to night. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ⚠️ Unable to check for merge conflicts: Invalid branch name format
Description check ⚠️ Warning PR description includes embedded images and multiple Sourcery-generated summaries but lacks a concise human-written description of what the PR does, the related issue number, specific changes, and testing details in the template format. Add a clear, concise description explaining the PR objective, list key changes, note any breaking changes, describe testing performed, and reference the related issue number following the provided template structure.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately summarizes the main feature: improving input mappings with conflict handling and unbound warnings, which aligns with the PR's primary objectives.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch enable-keyboard-&-dpad-navigation-for-key-mapping-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.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In _ensure_defaults_saved, the ternary-style cast assignment to device is a bit hard to read; consider splitting the InputEventJoypadButton vs InputEventJoypadMotion branches and setting device explicitly in each for clarity and easier debugging.
  • When reading def['type'] from DEFAULT_GAMEPAD[action], there’s no fallback if an unexpected type is configured; adding a final else branch that logs a warning could make misconfigured defaults easier to detect.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_ensure_defaults_saved`, the ternary-style cast assignment to `device` is a bit hard to read; consider splitting the `InputEventJoypadButton` vs `InputEventJoypadMotion` branches and setting `device` explicitly in each for clarity and easier debugging.
- When reading `def['type']` from `DEFAULT_GAMEPAD[action]`, there’s no fallback if an unexpected type is configured; adding a final `else` branch that logs a warning could make misconfigured defaults easier to detect.

## Individual Comments

### Comment 1
<location> `scripts/settings.gd:370` </location>
<code_context>
+				(nev as InputEventJoypadMotion).axis = def["axis"]
+				(nev as InputEventJoypadMotion).axis_value = def["value"]
+
+			(nev as InputEventJoypadButton if nev is InputEventJoypadButton else nev as InputEventJoypadMotion).device = -1
+			InputMap.action_add_event(action, nev)
+			changed = true
</code_context>

<issue_to_address>
**suggestion:** Simplify the device assignment to avoid complex casting expression

The ternary cast here is hard to read and will fail if `nev` ever becomes another `InputEvent` type. Since `nev` is only `InputEventJoypadButton` or `InputEventJoypadMotion` here, set `device` directly in each branch where `nev` is created, or use a straightforward `if nev is InputEventJoypadButton` / `elif nev is InputEventJoypadMotion` before assigning `device` to keep the types explicit and avoid complex inline casting.

Suggested implementation:

```
			if nev is InputEventJoypadButton:
				(nev as InputEventJoypadButton).device = -1
			elif nev is InputEventJoypadMotion:
				(nev as InputEventJoypadMotion).device = -1

			InputMap.action_add_event(action, nev)
			changed = true

```

If there are other locations in this file (e.g. the keyboard defaults section or other mapping blocks) that use a similar inline cast pattern for `device` assignment, consider applying the same refactor there for consistency and readability.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/settings.gd Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@scenes/key_mapping_menu.tscn`:
- Around line 248-257: The LeftInputRemap button has focus_neighbor_left
incorrectly pointing to MenuUpInputRemap (which is in the right column); remove
or clear the focus_neighbor_left property on the LeftInputRemap node so left
D-pad presses don't jump right, and if you want wrapping instead add/set the
reciprocal focus_neighbor_right on the MenuUpInputRemap node to point to
LeftInputRemap (use the node names LeftInputRemap and MenuUpInputRemap to find
and update the properties).
- Around line 496-515: The ControlResetButton's focus_next and focus_previous
both point to ControlsBackButton, causing Tab/Shift-Tab to loop only between
these two buttons; update the ControlResetButton node (properties focus_next and
focus_previous) to point to the appropriate neighboring controls in the overall
tab order (e.g., the next/previous controls in KeyMapContainer or MenuKeyMap
such as MenuRightInputRemap or whatever logical sibling should follow/precede),
or remove these explicit focus_* assignments to allow the engine's default tab
order to handle traversal; also verify ControlsBackButton.focus_next still
points to ControlResetButton only if that bidirectional link is intended.

In `@scripts/settings.gd`:
- Around line 358-375: The block creates nev based on DEFAULT_GAMEPAD[action]
but leaves nev unassigned if def["type"] is unexpected and uses a long
concatenated log line; update the code in scripts/settings.gd around the nev
handling to validate def["type"] and bail out early (e.g., continue) when it's
not "button" or "axis", ensuring nev is always initialized before setting device
and calling InputMap.action_add_event(action, nev); also shorten the
Globals.log_message call (split the message or use a shorter string) to fix the
max-line-length lint error; reference symbols: DEFAULT_GAMEPAD, def, nev,
InputEventJoypadButton, InputEventJoypadMotion, InputMap.action_add_event,
Globals.log_message, and action.
🧹 Nitpick comments (1)
scripts/settings.gd (1)

332-379: Duplicated default-restoration logic.

The body of _ensure_defaults_saved() (Lines 335–375) is nearly identical to the default-adding block in load_input_mappings() (Lines 143–172). Both iterate ACTIONS, check for missing keyboard/gamepad events, and add defaults from the same dictionaries. Consider extracting a shared helper (e.g., _add_missing_defaults() -> bool) and calling it from both sites to keep the logic in one place.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13b44ba and 6f90be9.

📒 Files selected for processing (2)
  • scenes/key_mapping_menu.tscn
  • scripts/settings.gd
🧰 Additional context used
🪛 GitHub Actions: Pull Request Pipeline
scripts/settings.gd

[error] 370-370: gdlint: Max allowed line length (100) exceeded (max-line-length)

🔇 Additional comments (2)
scenes/key_mapping_menu.tscn (1)

14-27: LGTM on the new StyleBoxFlat focus styles.

The added focus style resources (StyleBoxFlat_4k3vu, StyleBoxFlat_mxrmg, StyleBoxFlat_rhvir, StyleBoxFlat_hh2p6) are consistent with the existing styling conventions and provide clear visual feedback for keyboard/D-pad navigation.

Also applies to: 49-55, 79-85

scripts/settings.gd (1)

62-67: This concern is incorrect—no duplication occurs due to protective conditional logic.

Both load_input_mappings() (lines 143–172) and _ensure_defaults_saved() (lines 348, 358) check for the presence of existing keyboard and gamepad events before adding defaults. The conditions if not has_key_event and if not has_gamepad ensure that defaults added by load_input_mappings() are not re-added by _ensure_defaults_saved().

The second function is redundant but not broken—it serves as a safety net for edge cases where defaults may have been removed between calls, but under normal operation, no duplicates are created.

Comment thread scenes/key_mapping_menu.tscn
Comment thread scenes/key_mapping_menu.tscn
Comment thread scripts/settings.gd Outdated
…xpression

The ternary cast here is hard to read and will fail if nev ever becomes another InputEvent type. Since nev is only InputEventJoypadButton or InputEventJoypadMotion here, set device directly in each branch where nev is created, or use a straightforward if nev is InputEventJoypadButton / elif nev is InputEventJoypadMotion before assigning device to keep the types explicit and avoid complex inline casting.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
scripts/settings.gd (1)

332-383: _ensure_defaults_saved() duplicates the default-backfill logic already in load_input_mappings() (lines 143–172).

Both functions iterate ACTIONS, check for missing keyboard/gamepad events, and add the same defaults from DEFAULT_KEYBOARD/DEFAULT_GAMEPAD. Since load_input_mappings() runs first and already backfills, _ensure_defaults_saved() will typically find nothing to do.

Consider having load_input_mappings() return a bool indicating whether defaults were added (or set an instance flag), and then save in _ready() based on that — removing this function entirely.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f90be9 and d8cd133.

📒 Files selected for processing (1)
  • scripts/settings.gd
🧰 Additional context used
🪛 GitHub Actions: Pull Request Pipeline
scripts/settings.gd

[error] 371-379: gdformat formatting check failed: 1 file would be reformatted. Run 'gdformat -i' to apply formatting changes.

🔇 Additional comments (1)
scripts/settings.gd (1)

62-67: LGTM on placement of the _ensure_defaults_saved() call.

Correct ordering: load mappings, backfill missing defaults (with its own save), then handle migration save separately.

Add a comprehensive web deployment guide for SkyLockAssault (Godot v4.5, Win10 x64). The new doc includes a platform comparison, reasons many portals lack publish APIs, a recommended phased automation workflow (automatic via itch.io/poki/viverse, semi-auto with Playwright for some portals, and manual targets), Godot Web export tips, and quickstart snippets for GitHub Actions and CLI-based uploads.
…atially inverted.

Line 250 sets focus_neighbor_left to MenuUpInputRemap, which is in the right (Menu) column. Pressing D-pad left from an item already in the left column would unexpectedly jump right. This should likely be unset (to block left navigation) or wrap to MenuUpInputRemap via focus_neighbor_right on that row instead.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@files/docs/Platforms_for_Web_Deployment_Guide.md`:
- Around line 3-46: Shorten or wrap long lines and fix markdown style: break the
overlong lines containing "Summary of zero-cost publishing platforms for Godot
v4.5 HTML5/WebGL games (Feb 2026)", the paragraph starting "We're building
**SkyLockAssault** — a totally free-to-play browser game in **Godot v4.5**" and
the "This `.md` file is your living playbook..." line so they are <=80 chars (or
wrap them with hard line breaks), replace the bold "Project Context" text with a
proper heading (change "**Project Context**" to "### Project Context"), and
reformat the table so the pipe alignment matches the header row (fix rows under
the header to use the same number of columns and aligned pipes) to resolve
MD013, MD036, and MD060 respectively.
🧹 Nitpick comments (2)
README.md (1)

80-80: Minor path inconsistency with other documentation links.

Most documentation entries (lines 63, 66–69) use a leading / (e.g., /files/docs/...), but this entry and line 65 omit it. Both forms work on GitHub, but consider making them consistent.

scripts/settings.gd (1)

332-383: _ensure_defaults_saved() largely duplicates the default-backfill logic already in load_input_mappings() (lines 143–172).

Both blocks iterate ACTIONS, check for missing keyboard/gamepad events, and add defaults from the same dictionaries using identical logic. This duplication means bug fixes (like the nev null guard) must be applied in two places.

Consider extracting a shared helper (e.g., _add_missing_defaults() -> bool) called from both sites, returning whether changes were made.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8cd133 and d1d10ad.

📒 Files selected for processing (4)
  • README.md
  • files/docs/Platforms_for_Web_Deployment_Guide.md
  • scenes/key_mapping_menu.tscn
  • scripts/settings.gd
🧰 Additional context used
🪛 GitHub Actions: Pull Request Pipeline
files/docs/Platforms_for_Web_Deployment_Guide.md

[error] 3-3: MD013/line-length Line length [Expected: 80; Actual: 88]

🪛 GitHub Check: Markdown Lint / lint (3.x)
files/docs/Platforms_for_Web_Deployment_Guide.md

[failure] 34-34: Table column style
files/docs/Platforms_for_Web_Deployment_Guide.md:34:162 MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md060.md


[failure] 34-34: Table column style
files/docs/Platforms_for_Web_Deployment_Guide.md:34:149 MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md060.md


[failure] 34-34: Table column style
files/docs/Platforms_for_Web_Deployment_Guide.md:34:108 MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md060.md


[failure] 34-34: Table column style
files/docs/Platforms_for_Web_Deployment_Guide.md:34:82 MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md060.md


[failure] 34-34: Table column style
files/docs/Platforms_for_Web_Deployment_Guide.md:34:60 MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md060.md


[failure] 34-34: Table column style
files/docs/Platforms_for_Web_Deployment_Guide.md:34:39 MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md060.md


[failure] 12-12: Line length
files/docs/Platforms_for_Web_Deployment_Guide.md:12:81 MD013/line-length Line length [Expected: 80; Actual: 88] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md


[failure] 7-7: Line length
files/docs/Platforms_for_Web_Deployment_Guide.md:7:81 MD013/line-length Line length [Expected: 80; Actual: 89] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md


[failure] 5-5: Emphasis used instead of a heading
files/docs/Platforms_for_Web_Deployment_Guide.md:5 MD036/no-emphasis-as-heading Emphasis used instead of a heading [Context: "Project Context"] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md036.md


[failure] 3-3: Line length
files/docs/Platforms_for_Web_Deployment_Guide.md:3:81 MD013/line-length Line length [Expected: 80; Actual: 88] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md

🪛 LanguageTool
files/docs/Platforms_for_Web_Deployment_Guide.md

[style] ~38-~38: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ... | Yes | ❌ No | ✅ Very easy | Instant publish | Simp...

(EN_WEAK_ADJECTIVE)

🔇 Additional comments (2)
scenes/key_mapping_menu.tscn (2)

248-257: Previous focus_neighbor_left issue on LeftInputRemap appears resolved.

The spatially-inverted focus_neighbor_left that previously pointed to the right column has been removed. Navigation from LeftInputRemap now correctly wires top/bottom/right neighbors only.


142-172: Overall navigation wiring looks well thought out.

The two-column layout with cross-column focus_neighbor_right/focus_neighbor_left links, proper top/bottom chaining, and bridging via focus_next from NextWeaponInputRemapPauseInputRemap provides a logical traversal order. The device toggle buttons (Keyboard/Gamepad) are correctly wired as entry points from the top.

Comment thread files/docs/Platforms_for_Web_Deployment_Guide.md Outdated
…ailure on Line 370.

If def["type"] is neither "button" nor "axis", nev remains uninitialized (declared but never assigned on Line 360). Lines 370–371 would then crash with a null access. Additionally, Line 370 exceeds the 100-character lint limit flagged by the pipeline.
@ikostan

ikostan commented Feb 17, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • There’s quite a bit of duplication between get_pause_binding_label and get_pause_binding_label_for_device in settings.gd (including the temporary InputRemapButton construction); consider centralizing the label resolution into a single helper that takes an optional preferred device to reduce maintenance surface.
  • Both Globals._ready() and Settings._ready() call Settings.load_last_input_device() (directly or indirectly), which means the config is loaded twice on startup; you could either let only one of these be the source of truth or guard the second call to avoid redundant I/O.
  • Several non-UI scripts (settings.gd) construct InputRemapButton nodes purely for label generation; extracting the label logic into a non-Control helper (e.g. a static utility or a method on Settings/Globals) would avoid creating UI nodes in core logic and simplify future refactors.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There’s quite a bit of duplication between `get_pause_binding_label` and `get_pause_binding_label_for_device` in `settings.gd` (including the temporary `InputRemapButton` construction); consider centralizing the label resolution into a single helper that takes an optional preferred device to reduce maintenance surface.
- Both `Globals._ready()` and `Settings._ready()` call `Settings.load_last_input_device()` (directly or indirectly), which means the config is loaded twice on startup; you could either let only one of these be the source of truth or guard the second call to avoid redundant I/O.
- Several non-UI scripts (`settings.gd`) construct `InputRemapButton` nodes purely for label generation; extracting the label logic into a non-Control helper (e.g. a static utility or a method on `Settings`/`Globals`) would avoid creating UI nodes in core logic and simplify future refactors.

## Individual Comments

### Comment 1
<location> `scripts/settings.gd:297-299` </location>
<code_context>
+	# For corrupt/no-file: config empty → add defaults.
+	# Sets _needs_migration=true if added → triggers save in _ready() (first-run/repair).
+	# Idempotent with _ensure_defaults_saved().
+	var defaults_config: ConfigFile = ConfigFile.new()
+	defaults_config.load(path)
+	if _add_missing_defaults(defaults_config):  # Still pass it (harmless)
+		_needs_save = true

</code_context>

<issue_to_address>
**suggestion (performance):** Avoid re-loading the config file and reuse the existing ConfigFile instance for defaults backfill.

In `load_input_mappings`, you load `config` from `path` and then create a new `defaults_config` and load the same file again just to call `_add_missing_defaults`. This duplicates I/O and re-triggers parse errors for corrupt files. Instead, pass the already-loaded `config` into `_add_missing_defaults` (or clone it if you truly need a separate instance) to avoid the redundant `load()` call.

Suggested implementation:

```
	# For corrupt/no-file: config empty → add defaults.
	# Sets _needs_migration=true if added → triggers save in _ready() (first-run/repair).
	# Idempotent with _ensure_defaults_saved().
+	if _add_missing_defaults(config):
+		_needs_save = true

```

This change assumes that within the same scope there is an existing, already-loaded `ConfigFile` instance named `config` (e.g. created and loaded earlier in `load_input_mappings` with `config.load(path)`). If the variable has a different name, update the `_add_missing_defaults(config)` call accordingly.
</issue_to_address>

### Comment 2
<location> `scripts/settings.gd:557-561` </location>
<code_context>
+
+## Returns true if there is at least one unbound critical action for the currently selected device.
+## This respects Globals.current_input_device.
+func has_unbound_critical_actions_for_current_device() -> bool:
+	var preferred: String = Globals.current_input_device
+	for action: String in CRITICAL_ACTIONS:
+		var events: Array[InputEvent] = InputMap.action_get_events(action)
+		if events.is_empty():
+			# continue  # action is unbound, but we check per device
+			return true  # unbound for all devices, including current
</code_context>

<issue_to_address>
**nitpick:** Clean up the commented-out `continue` to better reflect the intended control flow.

The early-return branch still contains a commented-out `continue` right before `return true`, which now conflicts with the actual behavior and can mislead future edits. Please either remove the stale comment or reword it to clearly justify the early return when `events.is_empty()`.
</issue_to_address>

### Comment 3
<location> `scripts/key_mapping.gd:87-91` </location>
<code_context>
 			)
 			js_window.controlsBackPressed = _controls_back_button_pressed_cb

+	# Load and apply the last selected device (so it remembers across sessions)
+	Settings.load_last_input_device()
+	keyboard.button_pressed = (Globals.current_input_device == "keyboard")
+	gamepad.button_pressed = (Globals.current_input_device == "gamepad")
+	update_all_remap_buttons()
+
+
</code_context>

<issue_to_address>
**suggestion (performance):** Consider avoiding duplicate calls to Settings.load_last_input_device to reduce redundant config reads.

`Globals._ready()` already calls `Settings.load_last_input_device()`, and `key_mapping.gd` calls it again here, so the config is read twice on startup. Since `Globals.current_input_device` is already populated before this menu opens, you can just use it to set `keyboard.button_pressed` / `gamepad.button_pressed` and update the remap buttons, without reloading from disk. If you do need to query Settings here, consider adding a lightweight accessor that doesn’t hit the filesystem again.

```suggestion
	# Apply the last selected device (Globals has already loaded it in _ready)
	keyboard.button_pressed = (Globals.current_input_device == "keyboard")
	gamepad.button_pressed = (Globals.current_input_device == "gamepad")
	update_all_remap_buttons()
```
</issue_to_address>

### Comment 4
<location> `scripts/main_menu.gd:170` </location>
<code_context>
-	Globals.load_scene_with_loading("res://scenes/main_scene.tscn")
+	if Settings.has_unbound_critical_actions_for_current_device():  # ← FIXED
+		# Show warning dialog (create once or use existing ConfirmationDialog)
+		var dialog: ConfirmationDialog = ConfirmationDialog.new()
+		dialog.title = "Unbound Controls"
+		dialog.dialog_text = "Some critical controls are unbound.\nGo to Key Mapping to fix?"
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Re-use a single confirmation dialog instead of creating a new one on every Start press.

In `_on_start_pressed` a new `ConfirmationDialog` is created and added every time Start is pressed while critical actions are unbound. Repeated presses will stack multiple dialogs and signal connections in the scene tree. Instead, create a single dialog (e.g. as an `onready` node or lazily with a stored reference) and just configure it and call `popup_centered()` when needed.

Suggested implementation:

```
	## Handles the Start button press.
	##
	## :param _args: Optional arguments from web overlays (unused).
	## :type _args: Array
	## :rtype: void
	Globals.log_message("Start Game menu button pressed.", Globals.LogLevel.DEBUG)
	if Settings.has_unbound_critical_actions_for_current_device():  # ← FIXED
		# Show (or create) a single reusable warning dialog about unbound controls.
		if unbound_controls_dialog == null:
			unbound_controls_dialog = ConfirmationDialog.new()
			unbound_controls_dialog.title = "Unbound Controls"
			unbound_controls_dialog.dialog_text = "Some critical controls are unbound.\nGo to Key Mapping to fix?"
			unbound_controls_dialog.get_ok_button().text = "Open Key Mapping"
			unbound_controls_dialog.get_cancel_button().text = "Start Anyway"
			add_child(unbound_controls_dialog)

			# Continue to Key Mapping menu
			unbound_controls_dialog.confirmed.connect(_on_unbound_controls_dialog_confirmed)

		# Ensure dialog text/buttons are correct before showing (in case they change in future).
		unbound_controls_dialog.dialog_text = "Some critical controls are unbound.\nGo to Key Mapping to fix?"
		unbound_controls_dialog.get_ok_button().text = "Open Key Mapping"
		unbound_controls_dialog.get_cancel_button().text = "Start Anyway"

		unbound_controls_dialog.popup_centered()
		return
	# Open the gameplay anyway

```

To fully implement this:

1. Add a member variable to `scripts/main_menu.gd` (near the top of the script):
   ```gdscript
   var unbound_controls_dialog: ConfirmationDialog = null
   ```

2. Implement the handler used in the `confirmed` connection:
   ```gdscript
   func _on_unbound_controls_dialog_confirmed() -> void:
       Globals.load_key_mapping(ui_panel)  # same as options menu
   ```

3. Ensure that after the `if Settings.has_unbound_critical_actions_for_current_device():` block, the existing logic that starts the game (previously after the `# Open the gameplay anyway` comment) still runs when controls are bound (and that the `return` added in the replacement keeps the "start game" path from executing when the dialog is shown).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/settings.gd Outdated
Comment thread scripts/settings.gd
Comment thread scripts/key_mapping.gd Outdated
Comment thread scripts/main_menu.gd Outdated
ikostan and others added 3 commits February 17, 2026 19:11
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Avoid loading a separate defaults ConfigFile when ensuring input defaults — call _add_missing_defaults(config) directly and mark _needs_save if defaults were added. Also remove the call to update_all_remap_buttons() from key_mapping.gd's _ready(), relying on the default keyboard selection/focus instead.
… creating a new one on every Start press.

In _on_start_pressed a new ConfirmationDialog is created and added every time Start is pressed while critical actions are unbound. Repeated presses will stack multiple dialogs and signal connections in the scene tree. Instead, create a single dialog (e.g. as an onready node or lazily with a stored reference) and just configure it and call popup_centered() when needed.
@ikostan

ikostan commented Feb 18, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai title

@sourcery-ai sourcery-ai Bot changed the title Enable keyboard & dpad navigation for key mapping settings Improve input mappings with conflict handling and unbound warnings Feb 18, 2026
Make the unbound dialog modal (exclusive) and refactor its signal handling to avoid race conditions with the Start button and scene loading. Confirmed now loads key mapping and hides the dialog; cancel uses the cancel button's pressed signal to hide then load the main scene; close_requested simply hides and logs a debug message. Removed per-handler re-enabling of the Start button and added a visibility_changed handler that reliably re-enables and call_deferred("grab_focus") on the Start button after the dialog is fully hidden.
There’s quite a bit of duplication between get_pause_binding_label and get_pause_binding_label_for_device in settings.gd (including the temporary InputRemapButton construction); consider centralizing the label resolution into a single helper that takes an optional preferred device to reduce maintenance surface.
Both Globals._ready() and Settings._ready() call Settings.load_last_input_device() (directly or indirectly), which means the config is loaded twice on startup; you could either let only one of these be the source of truth or guard the second call to avoid redundant I/O.
@ikostan

ikostan commented Feb 18, 2026

Copy link
Copy Markdown
Owner Author

Addressing following:

  • There’s quite a bit of duplication between get_pause_binding_label and get_pause_binding_label_for_device in settings.gd (including the temporary InputRemapButton construction); consider centralizing the label resolution into a single helper that takes an optional preferred device to reduce maintenance surface.
  • Both Globals._ready() and Settings._ready() call Settings.load_last_input_device() (directly or indirectly), which means the config is loaded twice on startup; you could either let only one of these be the source of truth or guard the second call to avoid redundant I/O.

@ikostan

ikostan commented Feb 18, 2026

Copy link
Copy Markdown
Owner Author

Working on:

  • Several non-UI scripts (settings.gd) construct InputRemapButton nodes purely for label generation; extracting the label logic into a non-Control helper (e.g. a static utility or a method on Settings/Globals) would avoid creating UI nodes in core logic and simplify future refactors.

Several non-UI scripts (settings.gd) construct InputRemapButton nodes purely for label generation; extracting the label logic into a non-Control helper (e.g. a static utility or a method on Settings/Globals) would avoid creating UI nodes in core logic and simplify future refactors.

To address the feedback, add this static method to settings.gd (or a new autoload like InputUtils.gd for broader reuse). It replicates InputRemapButton.get_event_label() logic without instantiating a UI node

Important for decoupling UI from core logic, improving maintainability. Address in current PR #409, as it already refactors settings.gd for input mappings and unbound states—fits scope without new issue.
@ikostan

ikostan commented Feb 18, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The get_pause_binding_label_for_device API and its tests use mixed device identifiers ("keyboard"/"gamepad" in the implementation vs. "joypad" in tests), which will only hit the fallback path for gamepad in some cases—consider normalizing accepted values (e.g., constants or an enum) and updating callers/tests consistently to avoid subtle mislabeling.
  • Several tests around settings/device persistence (e.g., EC-09 and the new pause-label tests) manipulate Settings.CONFIG_PATH files directly with ad‑hoc backup/restore logic; it would be more robust to add an injectable config path or helper in Settings so tests don't touch the real user config and can avoid duplicating backup code.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `get_pause_binding_label_for_device` API and its tests use mixed device identifiers (`"keyboard"`/`"gamepad"` in the implementation vs. `"joypad"` in tests), which will only hit the fallback path for gamepad in some cases—consider normalizing accepted values (e.g., constants or an enum) and updating callers/tests consistently to avoid subtle mislabeling.
- Several tests around settings/device persistence (e.g., EC-09 and the new pause-label tests) manipulate `Settings.CONFIG_PATH` files directly with ad‑hoc backup/restore logic; it would be more robust to add an injectable config path or helper in `Settings` so tests don't touch the real user config and can avoid duplicating backup code.

## Individual Comments

### Comment 1
<location> `scripts/main_scene.gd:45-54` </location>
<code_context>
 # :type event: InputEvent
 # :rtype: void
-# In input_remap_button.gd (full updated _input function)
 func _input(event: InputEvent) -> void:
 	if not listening:
 		return
</code_context>

<issue_to_address>
**issue (bug_risk):** Unbound-key message may fire for UI-only bindings like pause or cancel, which might already be mapped.

Settings.is_event_bound() only checks ACTIONS and ignores UI actions like ui_cancel/ui_accept. In _input(), a key mapped only to a UI action (e.g. Esc → ui_cancel) is therefore treated as unbound and triggers KEY_PRESS_UNBOUND even though it’s a valid binding. To avoid this, either iterate over InputMap.get_actions() in is_event_bound(), or add a helper that checks “bound to any action” and use that for this case.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/main_scene.gd
The get_pause_binding_label_for_device API and its tests use mixed device identifiers ("keyboard"/"gamepad" in the implementation vs. "joypad" in tests), which will only hit the fallback path for gamepad in some cases—consider normalizing accepted values (e.g., constants or an enum) and updating callers/tests consistently to avoid subtle mislabeling.
@ikostan

ikostan commented Feb 18, 2026

Copy link
Copy Markdown
Owner Author

Addressing following:

  • The get_pause_binding_label_for_device API and its tests use mixed device identifiers ("keyboard"/"gamepad" in the implementation vs. "joypad" in tests), which will only hit the fallback path for gamepad in some cases—consider normalizing accepted values (e.g., constants or an enum) and updating callers/tests consistently to avoid subtle mislabeling.

The suggestion fixes a bug where is_event_bound() in settings.gd only checks the custom ACTIONS array (e.g., "fire", "pause", "ui_accept"), ignoring Godot's built-in UI actions like "ui_cancel" or "ui_focus_next". If a key (e.g., Esc) is bound only to "ui_cancel" (not in ACTIONS), the function returns false, wrongly triggering the unbound-key message in main_scene.gd's _input().
To fix: Change the loop in is_event_bound() to use InputMap.get_actions() (all actions) instead of ACTIONS. This ensures keys bound to any action (custom or UI) are detected as bound.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment