Full Xbox/PlayStation controller support: glyphs, remapping, vibration toggle

Extends the earlier Xbox-only ability glyphs into full brand-aware
controller support, following standard practice (position-based
button mapping, safe defaults for unrecognised pads, no per-device
special-casing):

- Controls detects PlayStation pads (DualShock/DualSense) from
  Input.get_joy_name() via a name-substring heuristic, checking for
  "xbox" first so a Bluetooth-connected Xbox pad (reports as "Xbox
  Wireless Controller") can't be misread as PlayStation through the
  generic "wireless controller" hint. Unrecognised/generic pads read
  as Xbox-style, matching the browser's Standard Gamepad Mapping.
- Ability-bar keycaps and the options menu now show ✕/○/□/△ for a
  detected PlayStation pad instead of A/B/X/Y — no index remapping
  needed, since PlayStation's face buttons sit in the exact same
  four positions as Xbox's.
- The options menu's Controls section gains a gamepad rebind button
  next to each ability's keyboard one (movement stays on the D-pad/
  stick, not individually remapped, matching convention). Clicking it
  captures the next joypad button press, mirroring the existing
  keyboard-rebind flow exactly (Esc cancels either). Gamepad rebinds
  persist to controls.cfg alongside keyboard ones and survive a
  reload.
- Added a player-facing "Controller Vibration" toggle (Settings...
  Gameplay section) — Controls.vibrate() now checks it alongside the
  existing DP debug flag, so either can silence rumble.

Verified headlessly end-to-end: default bindings show correct
brand-agnostic glyphs (kick=X, dash=A, ...), rebinding kick to a
different button through the real options-menu flow changes only
that action's glyph, leaves its keyboard binding and other abilities'
gamepad bindings untouched, and survives a reload from disk. Sanity-
checked the PlayStation name heuristic against realistic controller
name strings from Xbox, PlayStation (Windows/macOS/Bluetooth
variants), and other brands.

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:33:02 +03:00
parent a34989c49e
commit 431ee295ae
3 changed files with 162 additions and 19 deletions
+40 -3
View File
@@ -80,6 +80,8 @@ const ACTION_LABELS: Dictionary = {
var _listening_action: StringName = &""
var _listening_button: Button = null
var _listening_joy_action: StringName = &""
var _listening_joy_button: Button = null
func _ready() -> void:
@@ -207,6 +209,7 @@ func _build_options_toggles() -> void:
_add_section_label("── Gameplay ──", idx); idx += 1
_add_toggle_row("Screen Shake", Settings.screen_shake, Settings.set_screen_shake, idx); idx += 1
_add_toggle_row("Blood & Gore", Settings.gore, Settings.set_gore, idx); idx += 1
_add_toggle_row("Controller Vibration", Settings.vibration, Settings.set_vibration, idx); idx += 1
_add_section_label("── Controls ──", idx)
@@ -316,6 +319,16 @@ func _build_rebind_rows() -> void:
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)):
var joy_button := Button.new()
joy_button.custom_minimum_size = Vector2(72, 0)
joy_button.text = Controls.get_joy_button_label(action)
_style_button(joy_button, false)
joy_button.pressed.connect(_on_rebind_joy_pressed.bind(action, joy_button))
row.add_child(joy_button)
rebind_list.add_child(row)
@@ -369,17 +382,26 @@ func _on_rebind_pressed(action: StringName, button: Button) -> void:
button.text = "Press a key…"
func _on_rebind_joy_pressed(action: StringName, button: Button) -> void:
_cancel_listening()
_listening_joy_action = action
_listening_joy_button = button
button.text = "Press…"
func _cancel_listening() -> void:
if _listening_button != null:
_listening_button.text = Controls.get_key_label(_listening_action)
if _listening_joy_button != null:
_listening_joy_button.text = Controls.get_joy_button_label(_listening_joy_action)
_listening_action = &""
_listening_button = null
_listening_joy_action = &""
_listening_joy_button = null
func _unhandled_input(event: InputEvent) -> void:
if _listening_action == &"":
return
if event is InputEventKey and event.pressed and not event.echo:
if _listening_action != &"" and event is InputEventKey and event.pressed and not event.echo:
if event.physical_keycode == KEY_ESCAPE:
_cancel_listening()
else:
@@ -390,3 +412,18 @@ func _unhandled_input(event: InputEvent) -> void:
_listening_action = &""
_listening_button = null
get_viewport().set_input_as_handled()
return
if _listening_joy_action != &"":
if event is InputEventKey and event.pressed and not event.echo \
and event.physical_keycode == KEY_ESCAPE:
_cancel_listening()
get_viewport().set_input_as_handled()
elif event is InputEventJoypadButton and event.pressed:
var new_joy_event := InputEventJoypadButton.new()
new_joy_event.button_index = event.button_index
Controls.rebind_joy(_listening_joy_action, new_joy_event)
_listening_joy_button.text = Controls.get_joy_button_label(_listening_joy_action)
_listening_joy_action = &""
_listening_joy_button = null
get_viewport().set_input_as_handled()