diff --git a/systems/input/controls_manager.gd b/systems/input/controls_manager.gd index cd0da6e..296cc1d 100644 --- a/systems/input/controls_manager.gd +++ b/systems/input/controls_manager.gd @@ -27,6 +27,64 @@ func _ready() -> void: _install_web_fullscreen_hook() +# ── Active input device ────────────────────────────────────────────────────────── +# Tracks whether the player's last deliberate input was a gamepad or keyboard/mouse, so the +# HUD can swap its ability-bar keycaps to match whichever the player is actually holding. +# Mouse motion is deliberately ignored — the mouse still drives the camera regardless of +# which device is moving the bull, so panning the camera shouldn't flip the hint away from +# gamepad glyphs. A small deadzone on joypad axes avoids stick drift/noise doing the same. +const _JOY_AXIS_DEADZONE := 0.5 +var _last_input_is_gamepad: bool = false + + +func _input(event: InputEvent) -> void: + if event is InputEventJoypadButton: + if event.pressed: + _last_input_is_gamepad = true + elif event is InputEventJoypadMotion: + if absf(event.axis_value) > _JOY_AXIS_DEADZONE: + _last_input_is_gamepad = true + elif event is InputEventKey: + if event.pressed: + _last_input_is_gamepad = false + elif event is InputEventMouseButton: + if event.pressed: + _last_input_is_gamepad = false + + +## Whether the player's last button/key/stick press was on a gamepad rather than +## keyboard/mouse. Drives which glyph set the HUD's ability keycaps show. +func using_gamepad() -> bool: + return _last_input_is_gamepad + + +# Godot's joypad button indices are already Xbox-position-normalized — by SDL on native, by +# the browser's Standard Gamepad Mapping on web — so this reads correctly for any controller +# brand (a Nintendo pad's physically-labelled "A" is button 1 here, same as "B" on Xbox). +const _XBOX_BUTTON_GLYPHS := { + JOY_BUTTON_A: "A", + JOY_BUTTON_B: "B", + JOY_BUTTON_X: "X", + JOY_BUTTON_Y: "Y", +} + + +## Xbox-style face-button glyph ("A"/"B"/"X"/"Y") for the action's bound joypad button, or +## "—" if it has none. +func get_joy_button_label(action: StringName) -> String: + for event: InputEvent in InputMap.action_get_events(action): + if event is InputEventJoypadButton: + return _XBOX_BUTTON_GLYPHS.get(event.button_index, "?") + return "—" + + +## Ability-bar keycap label for `action`: the bound Xbox-style face button while a gamepad +## is in use, else the bound keyboard key — so the on-screen prompt always matches whichever +## the player is actually holding. +func ability_key_label(action: StringName) -> String: + return get_joy_button_label(action) if using_gamepad() else get_key_label(action) + + ## Godot's built-in ui_accept (menu/button confirm) ships with zero joypad bindings — ## checked directly: only Enter/Kp Enter/Space. ui_left/right/up/down, by contrast, do ## default to the D-pad and stick, which is why gamepad focus navigation on a menu works out diff --git a/ui/hud.gd b/ui/hud.gd index ee010c5..5543eb8 100644 --- a/ui/hud.gd +++ b/ui/hud.gd @@ -44,7 +44,11 @@ var _hp_seg_fills: Array[ColorRect] = [] var _bottom_col: VBoxContainer var _ability_bar: HBoxContainer var _ability_panels: Array[PanelContainer] = [] +var _ability_key_labels: Array[Label] = [] var _bull_bg: TextureRect +# Cached so the ability keycaps only get rewritten on an actual keyboard<->gamepad switch, +# not every frame. +var _showing_gamepad_hints: bool = false # Lobby barrel-smash reward — tally the barrels present on load; when the last one bursts, # throw confetti + a "WOooOoW!" and grant the bull two yellow bonus pips. Latched so it @@ -257,6 +261,7 @@ static func _format_time(seconds: float) -> String: func _process(_delta: float) -> void: _update_control_hints() + _update_ability_key_labels() if _run_timer_label != null: _run_timer_label.visible = Run.run_timer_started() and not _game_over if _run_timer_label.visible: @@ -469,6 +474,18 @@ func _update_control_hints() -> void: _tab_hint.visible = not _controls_visible +# Swaps the ability-bar keycaps between keyboard letters and Xbox-style face-button glyphs +# (A/B/X/Y) as the player switches between keyboard and gamepad. Only rewrites the labels on +# an actual switch, not every frame — Controls.using_gamepad() itself is cheap either way. +func _update_ability_key_labels() -> void: + var gamepad := Controls.using_gamepad() + if gamepad == _showing_gamepad_hints: + return + _showing_gamepad_hints = gamepad + for i in _ability_key_labels.size(): + _ability_key_labels[i].text = Controls.ability_key_label(_ABILITY_ACTIONS[i]) + + # ── Damage vignette ─────────────────────────────────────────────────────────── # A full-screen ColorRect driven by damage_vignette.gdshader: transparent centre, red # toward the edges, alpha ("flash") tweened up on a hit and eased back to nothing. Built @@ -902,7 +919,7 @@ func _build_ability_slot(idx: int) -> Control: vbox.add_child(keycap) var key_lbl := Label.new() - key_lbl.text = Controls.get_key_label(_ABILITY_ACTIONS[idx]) + key_lbl.text = Controls.ability_key_label(_ABILITY_ACTIONS[idx]) key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER key_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER key_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -912,6 +929,7 @@ func _build_ability_slot(idx: int) -> Control: key_lbl.add_theme_constant_override("outline_size", 3) key_lbl.add_theme_font_size_override("font_size", 14) keycap.add_child(key_lbl) + _ability_key_labels.append(key_lbl) return vbox