From 02aec4249fd6eadc50cac820bd8bcd555b5e6b73 Mon Sep 17 00:00:00 2001 From: Richard Aasa Date: Thu, 10 Sep 2026 19:42:59 +0300 Subject: [PATCH] Make the Controls options menu device-aware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Showing both a keyboard key and a gamepad button on every ability row (and WASD movement rows that never had a gamepad equivalent anyway) cluttered the list once a controller is in play. The rebind list now shows only the relevant set for whichever device the player is actually holding: on a gamepad, movement rows disappear entirely (the D-pad/stick isn't individually remapped, so there's nothing to show) and each ability shows only its gamepad button; on keyboard, the reverse. Uses the same Controls.using_gamepad() last-input heuristic as the HUD's ability keycaps, not mere connection presence, so a controller left plugged in but unused doesn't hide keyboard binds from a mouse-and-keyboard player. The list (and its hint text) also swaps live if the player switches device while the panel is open, skipped while a rebind capture is in progress so it can't be torn out from under them. Verified headlessly: keyboard mode shows all 8 rows, gamepad mode shows only the 4 ability rows, and the hint text swaps between them — confirmed with a real screenshot of both states. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV --- ui/main_menu.gd | 55 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/ui/main_menu.gd b/ui/main_menu.gd index f27810f..ecff07f 100644 --- a/ui/main_menu.gd +++ b/ui/main_menu.gd @@ -82,6 +82,9 @@ var _listening_action: StringName = &"" var _listening_button: Button = null var _listening_joy_action: StringName = &"" var _listening_joy_button: Button = null +# Cached so the rebind list only rebuilds on an actual keyboard<->gamepad switch, not +# every frame the Options panel is open. +var _rebind_rows_show_gamepad: bool = false func _ready() -> void: @@ -186,6 +189,7 @@ func _plaque(hot: bool) -> StyleBoxFlat: func _process(_delta: float) -> void: + _update_rebind_rows_for_device() if not main_panel.visible or video.stream == null: return var frame_count := CAPE_POS.size() @@ -294,10 +298,24 @@ func _add_toggle_row(label_text: String, value: bool, setter: Callable, at_index options_vbox.move_child(row, at_index) +## Rebuilds the Controls rebind list for whichever device the player is actually holding +## (Controls.using_gamepad(), the same last-input heuristic the HUD's ability keycaps use — +## not mere connection presence, so a controller left plugged in but unused doesn't hide +## keyboard rebinds from a mouse-and-keyboard player). On a gamepad: movement rows drop +## entirely (the D-pad/stick isn't individually remapped, so there's nothing to show), and +## each ability shows only its gamepad button. On keyboard: the reverse — WASD-style +## keyboard binds only, no gamepad buttons cluttering the list. func _build_rebind_rows() -> void: for child in rebind_list.get_children(): child.queue_free() + var gamepad := Controls.using_gamepad() + _rebind_rows_show_gamepad = gamepad + _update_rebind_hint(gamepad) for action: StringName in ACTION_LABELS: + var is_joy_rebindable := Controls.JOY_REBINDABLE_ACTIONS.has(String(action)) + if gamepad and not is_joy_rebindable: + continue # movement: nothing to show/rebind on a gamepad + var row := HBoxContainer.new() row.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -312,16 +330,15 @@ func _build_rebind_rows() -> void: name_label.add_theme_constant_override("outline_size", 5) row.add_child(name_label) - var key_button := Button.new() - key_button.custom_minimum_size = Vector2(160, 0) - key_button.text = Controls.get_key_label(action) - _style_button(key_button, false) - key_button.pressed.connect(_on_rebind_pressed.bind(action, key_button)) - row.add_child(key_button) + if not gamepad: + var key_button := Button.new() + key_button.custom_minimum_size = Vector2(160, 0) + key_button.text = Controls.get_key_label(action) + _style_button(key_button, false) + key_button.pressed.connect(_on_rebind_pressed.bind(action, key_button)) + row.add_child(key_button) - # Movement stays on the D-pad/stick (standard, not individually remapped) — only the - # face-button abilities get a gamepad rebind button, alongside the keyboard one. - if Controls.JOY_REBINDABLE_ACTIONS.has(String(action)): + if gamepad and is_joy_rebindable: var joy_button := Button.new() joy_button.custom_minimum_size = Vector2(72, 0) joy_button.text = Controls.get_joy_button_label(action) @@ -332,6 +349,26 @@ func _build_rebind_rows() -> void: rebind_list.add_child(row) +func _update_rebind_hint(gamepad: bool) -> void: + var hint := get_node_or_null("OptionsPanel/VBox/Hint") as Label + if hint != null: + hint.text = "Click a button, then press a new one on your controller. Esc cancels." \ + if gamepad else "Click a key, then press a new key. Esc cancels." + + +# Live-swaps the rebind list if the player switches device while the Options panel is open +# (e.g. they were clicking through with a mouse, then pick up a controller). Skipped while +# actively listening for a rebind so an in-progress capture never gets torn out from under +# the player. +func _update_rebind_rows_for_device() -> void: + if not options_panel.visible: + return + if _listening_action != &"" or _listening_joy_action != &"": + return + if Controls.using_gamepad() != _rebind_rows_show_gamepad: + _build_rebind_rows() + + func _on_play_pressed() -> void: _set_buttons_disabled(true) Run.start_new_run() # fresh map for this run; the arena is the intro fight