Make the Controls options menu device-aware
CI / Decide whether to run (push) Has been cancelled
CI / Lint & headless tests (push) Has been cancelled

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV
This commit is contained in:
2026-09-10 19:42:59 +03:00
parent 431ee295ae
commit 02aec4249f
+46 -9
View File
@@ -82,6 +82,9 @@ var _listening_action: StringName = &""
var _listening_button: Button = null var _listening_button: Button = null
var _listening_joy_action: StringName = &"" var _listening_joy_action: StringName = &""
var _listening_joy_button: Button = null 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: func _ready() -> void:
@@ -186,6 +189,7 @@ func _plaque(hot: bool) -> StyleBoxFlat:
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
_update_rebind_rows_for_device()
if not main_panel.visible or video.stream == null: if not main_panel.visible or video.stream == null:
return return
var frame_count := CAPE_POS.size() 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) 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: func _build_rebind_rows() -> void:
for child in rebind_list.get_children(): for child in rebind_list.get_children():
child.queue_free() child.queue_free()
var gamepad := Controls.using_gamepad()
_rebind_rows_show_gamepad = gamepad
_update_rebind_hint(gamepad)
for action: StringName in ACTION_LABELS: 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() var row := HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL 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) name_label.add_theme_constant_override("outline_size", 5)
row.add_child(name_label) row.add_child(name_label)
var key_button := Button.new() if not gamepad:
key_button.custom_minimum_size = Vector2(160, 0) var key_button := Button.new()
key_button.text = Controls.get_key_label(action) key_button.custom_minimum_size = Vector2(160, 0)
_style_button(key_button, false) key_button.text = Controls.get_key_label(action)
key_button.pressed.connect(_on_rebind_pressed.bind(action, key_button)) _style_button(key_button, false)
row.add_child(key_button) 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 if gamepad and is_joy_rebindable:
# face-button abilities get a gamepad rebind button, alongside the keyboard one.
if Controls.JOY_REBINDABLE_ACTIONS.has(String(action)):
var joy_button := Button.new() var joy_button := Button.new()
joy_button.custom_minimum_size = Vector2(72, 0) joy_button.custom_minimum_size = Vector2(72, 0)
joy_button.text = Controls.get_joy_button_label(action) joy_button.text = Controls.get_joy_button_label(action)
@@ -332,6 +349,26 @@ func _build_rebind_rows() -> void:
rebind_list.add_child(row) 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: func _on_play_pressed() -> void:
_set_buttons_disabled(true) _set_buttons_disabled(true)
Run.start_new_run() # fresh map for this run; the arena is the intro fight Run.start_new_run() # fresh map for this run; the arena is the intro fight