Improve Android controller and chat support

This commit is contained in:
Alexander Sellite 2026-08-03 20:49:19 -04:00
parent 3dc8ced5d2
commit ceef03eba5
24 changed files with 2485 additions and 128 deletions

View file

@ -20,6 +20,11 @@ const HANDLE_SIZE := Vector2(34, 42)
const HANDLE_GAP: float = 6.0
const COLLAPSED_REVEAL_WIDTH: float = 8.0
const HINT_EDGE_MARGIN: float = 4.0
const MOBILE_COMPACT_WIDTH: float = 620.0
const MOBILE_EXPANDED_WIDTH: float = 820.0
const MOBILE_COMPACT_HEIGHT: float = 220.0
const MOBILE_EXPANDED_HEIGHT: float = 390.0
const MOBILE_EDGE_MARGIN: float = 12.0
const CLOCK_SIZE := Vector2(116.0, 34.0)
const CLOCK_EDGE_MARGIN: float = 10.0
const WEATHER_ICON_SIZE := Vector2(34.0, 34.0)
@ -72,8 +77,10 @@ var _send_pending: bool = false
var _pending_send_body: String = ""
var _prior_movement: bool = true
var _prior_camera: bool = true
var _controller_refocused: bool = false
var _output_scale: float = 1.0
var _dock_right: bool = false
var _mobile_mode: bool = false
var _world_time: WorldTimeServiceType
var _world_weather: WorldWeatherServiceType
@ -125,6 +132,7 @@ func setup(
_world_weather.get_weather(),
_world_weather.get_seconds_remaining(),
)
set_mobile_mode(_settings.current_settings.chat_mobile_mode)
set_dock_right(_settings.current_settings.chat_dock_right)
_service.message_received.connect(_on_message)
_service.local_message_confirmed.connect(_on_local_message_confirmed)
@ -151,6 +159,7 @@ func open_chat() -> void:
return
if _presentation_state == PresentationState.COLLAPSED:
_set_presentation_state(_visible_state_before_collapse, true, true)
_controller_refocused = false
_opened = true
text_entry_ownership_changed.emit(true)
_prior_movement = _player.is_movement_enabled()
@ -162,8 +171,10 @@ func open_chat() -> void:
# prediction would let a remote host continue the last held movement.
_session.submit_neutral_local_movement()
_entry.show()
_entry.virtual_keyboard_enabled = false
_entry.grab_focus()
_hint.hide()
_refresh_input_ownership()
_update_panel_opacity(true)
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
@ -184,6 +195,7 @@ func close_chat() -> void:
return
_opened = false
text_entry_ownership_changed.emit(false)
_entry.virtual_keyboard_enabled = false
_entry.release_focus()
_entry.hide()
_player.set_movement_enabled(_prior_movement)
@ -191,14 +203,62 @@ func close_chat() -> void:
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
_flush_draft()
_refresh_visibility()
_refresh_input_ownership()
func toggle_chat() -> void:
if _presentation_state == PresentationState.COLLAPSED:
open_chat()
return
if _opened:
close_chat()
_set_presentation_state(PresentationState.COLLAPSED, true, true)
return
if _controller_refocused:
_controller_refocused = false
_set_presentation_state(PresentationState.COLLAPSED, true, true)
return
open_chat()
func refocus_gameplay() -> void:
close_chat()
_controller_refocused = true
get_viewport().gui_release_focus()
func toggle_focus() -> void:
if _presentation_state == PresentationState.COLLAPSED:
return
if _opened:
refocus_gameplay()
else:
open_chat()
func request_virtual_keyboard() -> bool:
if not _opened or not _entry.has_focus():
return false
_entry.virtual_keyboard_enabled = true
# Re-entering focus after the controller accept event is the explicit
# request Android uses to display its keyboard. Merely focusing Chat keeps
# the keyboard disabled so the world remains readable.
_entry.release_focus()
_entry.call_deferred("grab_focus")
return true
func is_open() -> bool:
return _opened
func is_collapsed() -> bool:
return _presentation_state == PresentationState.COLLAPSED
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("open_chat"):
if _opened:
close_chat()
elif _available:
open_chat()
toggle_chat()
get_viewport().set_input_as_handled()
return
if event is InputEventKey and event.pressed and not event.echo:
@ -404,14 +464,21 @@ func _build_ui() -> void:
_unread_indicator.hide()
_hint.hide()
_layout_presentation(false)
_refresh_input_ownership()
func _chat_panel_style() -> StyleBoxFlat:
var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94))
style.corner_radius_top_left = 10 if _dock_right else 0
style.corner_radius_bottom_left = 10 if _dock_right else 0
style.corner_radius_top_right = 0 if _dock_right else 10
style.corner_radius_bottom_right = 0 if _dock_right else 10
if _mobile_mode:
style.corner_radius_top_left = 0
style.corner_radius_top_right = 0
style.corner_radius_bottom_left = 10
style.corner_radius_bottom_right = 10
else:
style.corner_radius_top_left = 10 if _dock_right else 0
style.corner_radius_bottom_left = 10 if _dock_right else 0
style.corner_radius_top_right = 0 if _dock_right else 10
style.corner_radius_bottom_right = 0 if _dock_right else 10
style.content_margin_left = 12
style.content_margin_right = 12
style.content_margin_top = 10
@ -419,6 +486,18 @@ func _chat_panel_style() -> StyleBoxFlat:
return style
func _refresh_input_ownership() -> void:
if _panel == null or _history == null:
return
var filter: Control.MouseFilter = (
Control.MOUSE_FILTER_STOP
if _opened
else Control.MOUSE_FILTER_IGNORE
)
_panel.mouse_filter = filter
_history.mouse_filter = filter
func _clock_panel_style() -> StyleBoxFlat:
var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94))
style.set_corner_radius_all(12)
@ -620,7 +699,10 @@ func _refresh_visibility() -> void:
_collapse_button.show()
var collapsed := _presentation_state == PresentationState.COLLAPSED
_panel.show()
_height_button.show()
_height_button.visible = not _mobile_mode
_height_button.focus_mode = (
Control.FOCUS_NONE if _mobile_mode else Control.FOCUS_ALL
)
_unread_indicator.visible = collapsed and _collapsed_has_unread
_hint.visible = not _opened
@ -670,10 +752,31 @@ func is_docked_right() -> bool:
return _dock_right
func set_mobile_mode(enabled: bool) -> void:
_mobile_mode = enabled
if not is_node_ready() or _panel == null:
return
if _mobile_mode:
if _presentation_state == PresentationState.EXPANDED:
_presentation_state = PresentationState.COMPACT
if _visible_state_before_collapse == PresentationState.EXPANDED:
_visible_state_before_collapse = PresentationState.COMPACT
_panel.add_theme_stylebox_override("panel", _chat_panel_style())
_refresh_handle_labels(_presentation_state)
_refresh_visibility()
_layout_presentation(false)
func is_mobile_mode() -> bool:
return _mobile_mode
func _refresh_handle_labels(state: PresentationState) -> void:
var collapsed := state == PresentationState.COLLAPSED
var height_state := _visible_state_before_collapse if collapsed else state
if _dock_right:
if _mobile_mode:
_collapse_button.text = "v" if collapsed else "^"
elif _dock_right:
_collapse_button.text = "<" if collapsed else ">"
else:
_collapse_button.text = ">" if collapsed else "<"
@ -798,32 +901,101 @@ func _layout_presentation(animate: bool) -> void:
var viewport_height := size.y
if viewport_height <= 0.0:
viewport_height = 720.0
var layout_state := (
_visible_state_before_collapse
if _presentation_state == PresentationState.COLLAPSED
else _presentation_state
)
var collapsed := _presentation_state == PresentationState.COLLAPSED
var bottom_margin := minf(
BOTTOM_MARGIN,
maxf(MIN_TOP_MARGIN, (viewport_height - MIN_HISTORY_HEIGHT) * 0.25),
)
var bottom := viewport_height - bottom_margin
var height := COMPACT_HEIGHT
var layout_state := (
_visible_state_before_collapse
if _presentation_state == PresentationState.COLLAPSED
else _presentation_state
)
if layout_state == PresentationState.EXPANDED:
height = maxf(MIN_HISTORY_HEIGHT, viewport_height - 2.0 * bottom_margin)
height = minf(height, maxf(MIN_HISTORY_HEIGHT, bottom - MIN_TOP_MARGIN))
var collapsed := _presentation_state == PresentationState.COLLAPSED
var panel_x: float
if _dock_right:
panel_x = (
viewport_width - COLLAPSED_REVEAL_WIDTH
if collapsed
else viewport_width - PANEL_WIDTH
var panel_width := PANEL_WIDTH
var panel_x: float = 0.0
var panel_y: float = 0.0
var collapse_position := Vector2.ZERO
var height_position := Vector2.ZERO
var unread_position := Vector2.ZERO
if _mobile_mode:
panel_width = (
MOBILE_EXPANDED_WIDTH
if layout_state == PresentationState.EXPANDED
else MOBILE_COMPACT_WIDTH
)
panel_width = minf(
panel_width,
maxf(1.0, viewport_width - MOBILE_EDGE_MARGIN * 2.0),
)
height = (
MOBILE_EXPANDED_HEIGHT
if layout_state == PresentationState.EXPANDED
else MOBILE_COMPACT_HEIGHT
)
height = minf(
height,
maxf(MIN_HISTORY_HEIGHT, viewport_height - HANDLE_SIZE.y),
)
panel_x = (viewport_width - panel_width) * 0.5
panel_y = -(height - COLLAPSED_REVEAL_WIDTH) if collapsed else 0.0
var mobile_handle_y: float = (
COLLAPSED_REVEAL_WIDTH if collapsed else height
)
collapse_position = Vector2(
panel_x + panel_width - HANDLE_SIZE.x,
mobile_handle_y,
)
height_position = Vector2(
collapse_position.x - HANDLE_SIZE.x - HANDLE_GAP,
mobile_handle_y,
)
unread_position = collapse_position + Vector2(6.0, -18.0)
else:
panel_x = -(PANEL_WIDTH - COLLAPSED_REVEAL_WIDTH) if collapsed else 0.0
var target_position := Vector2(panel_x, bottom - height)
var target_size := Vector2(PANEL_WIDTH, height)
if layout_state == PresentationState.EXPANDED:
height = maxf(
MIN_HISTORY_HEIGHT,
viewport_height - 2.0 * bottom_margin,
)
height = minf(
height,
maxf(MIN_HISTORY_HEIGHT, bottom - MIN_TOP_MARGIN),
)
if _dock_right:
panel_x = (
viewport_width - COLLAPSED_REVEAL_WIDTH
if collapsed
else viewport_width - PANEL_WIDTH
)
else:
panel_x = (
-(PANEL_WIDTH - COLLAPSED_REVEAL_WIDTH)
if collapsed
else 0.0
)
panel_y = bottom - height
var handle_stack_height := HANDLE_SIZE.y * 2.0 + HANDLE_GAP
var handle_stack_top := (
bottom - COMPACT_HEIGHT * 0.5 - handle_stack_height * 0.5
)
var handle_x: float = (
panel_x - HANDLE_SIZE.x
if _dock_right
else COLLAPSED_REVEAL_WIDTH if collapsed else PANEL_WIDTH
)
collapse_position = Vector2(
handle_x,
handle_stack_top + HANDLE_SIZE.y + HANDLE_GAP,
)
height_position = Vector2(handle_x, handle_stack_top)
var unread_offset_x := -18.0 if _dock_right else 6.0
unread_position = collapse_position + Vector2(
unread_offset_x,
-18.0,
)
var target_position := Vector2(panel_x, panel_y)
var target_size := Vector2(panel_width, height)
var clock_x: float = (
viewport_width - CLOCK_SIZE.x - CLOCK_EDGE_MARGIN
if _dock_right else CLOCK_EDGE_MARGIN
@ -840,22 +1012,6 @@ func _layout_presentation(animate: bool) -> void:
var weather_icon_position := Vector2(
weather_icon_x, clock_position.y
)
var handle_stack_height := HANDLE_SIZE.y * 2.0 + HANDLE_GAP
var handle_stack_top := (
bottom - COMPACT_HEIGHT * 0.5 - handle_stack_height * 0.5
)
var handle_x: float
if _dock_right:
handle_x = panel_x - HANDLE_SIZE.x
else:
handle_x = COLLAPSED_REVEAL_WIDTH if collapsed else PANEL_WIDTH
var collapse_position := Vector2(
handle_x,
handle_stack_top + HANDLE_SIZE.y + HANDLE_GAP,
)
var height_position := Vector2(handle_x, handle_stack_top)
var unread_offset_x := -18.0 if _dock_right else 6.0
var unread_position := collapse_position + Vector2(unread_offset_x, -18.0)
var hint_position := Vector2(
(
viewport_width - _hint.size.x - HINT_EDGE_MARGIN

View file

@ -0,0 +1,450 @@
class_name ControllerMappingPanel
extends Control
signal closed
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
var _mapping_manager: ControllerMappingManagerType
var _binding_buttons: Dictionary = {}
var _controller_label: Label
var _instruction_label: Label
var _progress_label: Label
var _auto_map_button: Button
var _reset_button: Button
var _close_button: Button
var _capturing_role: StringName = &""
var _auto_map_active: bool = false
var _auto_map_index: int = -1
var _auto_map_draft: Dictionary = {}
func _ready() -> void:
set_process_input(true)
_build_interface()
hide()
func setup(mapping_manager: ControllerMappingManagerType) -> void:
_mapping_manager = mapping_manager
if not _mapping_manager.active_controller_changed.is_connected(
_on_active_controller_changed
):
_mapping_manager.active_controller_changed.connect(
_on_active_controller_changed
)
if not _mapping_manager.active_profile_changed.is_connected(
_refresh_bindings
):
_mapping_manager.active_profile_changed.connect(_refresh_bindings)
_refresh_bindings()
func open_panel() -> void:
if _mapping_manager == null:
return
_cancel_capture()
show()
_refresh_bindings()
_auto_map_button.grab_focus()
func is_open() -> bool:
return visible
func is_capturing() -> bool:
return visible and not _capturing_role.is_empty()
func request_back() -> void:
if not visible:
return
if not _capturing_role.is_empty():
_cancel_capture()
return
close_panel()
func close_panel() -> void:
_cancel_capture()
hide()
closed.emit()
func _input(event: InputEvent) -> void:
if not visible or _mapping_manager == null:
return
if _capturing_role.is_empty():
return
var key_event := event as InputEventKey
if (
key_event != null
and key_event.pressed
and not key_event.echo
and key_event.keycode == KEY_ESCAPE
):
get_viewport().set_input_as_handled()
_cancel_capture()
_progress_label.text = "controller mapping cancelled"
return
if not (
event is InputEventJoypadButton
or event is InputEventJoypadMotion
):
return
var binding: Dictionary = _mapping_manager.binding_from_event(
_capturing_role,
event,
)
if binding.is_empty():
return
if not _mapping_manager.validate_binding(_capturing_role, binding):
return
get_viewport().set_input_as_handled()
_accept_captured_binding(binding)
func _build_interface() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_STOP
z_index = 100
UtilityPageStyleType.apply_page(self)
var frame := PanelContainer.new()
frame.set_anchors_preset(Control.PRESET_FULL_RECT)
frame.offset_left = 18.0
frame.offset_top = 18.0
frame.offset_right = -18.0
frame.offset_bottom = -18.0
frame.add_theme_stylebox_override(
"panel",
UtilityPageStyleType.rounded_style(
UtilityPageStyleType.OCEAN_PANEL_DEEP,
20,
),
)
add_child(frame)
var outer_margin := MarginContainer.new()
outer_margin.add_theme_constant_override("margin_left", 24)
outer_margin.add_theme_constant_override("margin_top", 20)
outer_margin.add_theme_constant_override("margin_right", 24)
outer_margin.add_theme_constant_override("margin_bottom", 20)
frame.add_child(outer_margin)
var page := VBoxContainer.new()
page.add_theme_constant_override("separation", 12)
outer_margin.add_child(page)
var heading_row := HBoxContainer.new()
heading_row.add_theme_constant_override("separation", 12)
page.add_child(heading_row)
var title := Label.new()
title.text = "controller mapping"
title.add_theme_font_size_override("font_size", 28)
title.add_theme_color_override(
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
heading_row.add_child(title)
_controller_label = Label.new()
_controller_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_controller_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_controller_label.add_theme_font_size_override("font_size", 15)
_controller_label.add_theme_color_override(
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
)
heading_row.add_child(_controller_label)
_instruction_label = Label.new()
_instruction_label.text = (
"auto-map walks through a standard xbox-style controller. "
+ "select any row afterward to override it."
)
_instruction_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_instruction_label.add_theme_font_size_override("font_size", 15)
_instruction_label.add_theme_color_override(
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
)
page.add_child(_instruction_label)
_progress_label = Label.new()
_progress_label.custom_minimum_size.y = 34.0
_progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_progress_label.add_theme_font_size_override("font_size", 18)
_progress_label.add_theme_color_override(
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
)
page.add_child(_progress_label)
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
page.add_child(scroll)
var role_grid := GridContainer.new()
role_grid.columns = 2
role_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
role_grid.add_theme_constant_override("h_separation", 12)
role_grid.add_theme_constant_override("v_separation", 7)
scroll.add_child(role_grid)
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
var role_label := Label.new()
role_label.text = ControllerMappingManagerType.ROLE_LABELS.get(
role,
str(role),
)
role_label.custom_minimum_size = Vector2(330.0, 42.0)
role_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
role_label.add_theme_font_size_override("font_size", 16)
role_label.add_theme_color_override(
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
)
role_grid.add_child(role_label)
var binding_button := Button.new()
binding_button.custom_minimum_size = Vector2(210.0, 42.0)
binding_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
binding_button.focus_mode = Control.FOCUS_ALL
binding_button.tooltip_text = (
"change " + str(ControllerMappingManagerType.ROLE_LABELS.get(
role,
str(role),
))
)
UtilityPageStyleType.apply_compact_ocean_button(binding_button)
binding_button.pressed.connect(_begin_manual_capture.bind(role))
role_grid.add_child(binding_button)
_binding_buttons[role] = binding_button
var footer := HBoxContainer.new()
footer.alignment = BoxContainer.ALIGNMENT_END
footer.add_theme_constant_override("separation", 10)
page.add_child(footer)
_auto_map_button = Button.new()
_auto_map_button.text = "auto-map controller"
_auto_map_button.tooltip_text = (
"walk through every input on a standard xbox-style controller"
)
UtilityPageStyleType.apply_ocean_button(_auto_map_button)
_auto_map_button.pressed.connect(_begin_auto_map)
footer.add_child(_auto_map_button)
_reset_button = Button.new()
_reset_button.text = "restore defaults"
_reset_button.tooltip_text = "remove this controller's custom mapping"
UtilityPageStyleType.apply_ocean_button(_reset_button)
_reset_button.pressed.connect(_reset_mapping)
footer.add_child(_reset_button)
_close_button = Button.new()
_close_button.text = "done"
UtilityPageStyleType.apply_ocean_button(_close_button)
_close_button.pressed.connect(close_panel)
footer.add_child(_close_button)
func _begin_auto_map() -> void:
if _mapping_manager == null:
return
_auto_map_active = true
_auto_map_index = 0
_auto_map_draft = {}
_set_capture_role(ControllerMappingManagerType.ROLE_ORDER[_auto_map_index])
func _begin_manual_capture(role: StringName) -> void:
_auto_map_active = false
_auto_map_index = -1
_auto_map_draft.clear()
_set_capture_role(role)
func _set_capture_role(role: StringName) -> void:
_capturing_role = role
_set_action_buttons_disabled(true)
if _auto_map_active:
_progress_label.text = "step %d of %d: %s" % [
_auto_map_index + 1,
ControllerMappingManagerType.ROLE_ORDER.size(),
_mapping_manager.get_role_prompt(role),
]
return
var input_instruction: String = "press any controller button"
if _mapping_manager.role_expects_axis(role):
input_instruction = "move any controller axis"
elif _mapping_manager.role_accepts_axis(role):
input_instruction = "press any button or move any controller axis"
_progress_label.text = "%s for %s" % [
input_instruction,
_mapping_manager.get_role_label(role),
]
func _accept_captured_binding(binding: Dictionary) -> void:
if _auto_map_active:
_auto_map_draft[str(_capturing_role)] = binding.duplicate(true)
_auto_map_index += 1
if _auto_map_index < ControllerMappingManagerType.ROLE_ORDER.size():
_set_capture_role(
ControllerMappingManagerType.ROLE_ORDER[_auto_map_index]
)
return
var saved: bool = _mapping_manager.replace_active_bindings(
_auto_map_draft
)
_cancel_capture()
_progress_label.text = (
"controller mapped successfully"
if saved else "could not save the controller mapping"
)
_refresh_bindings()
return
var role: StringName = _capturing_role
var saved: bool = _mapping_manager.set_binding(role, binding)
_cancel_capture()
_progress_label.text = (
"updated " + _mapping_manager.get_role_label(role)
if saved else "could not save that controller input"
)
_refresh_bindings()
func _cancel_capture() -> void:
_capturing_role = &""
_auto_map_active = false
_auto_map_index = -1
_auto_map_draft.clear()
_set_action_buttons_disabled(false)
if _progress_label != null:
_progress_label.text = ""
func _set_action_buttons_disabled(disabled: bool) -> void:
if _auto_map_button != null:
_auto_map_button.disabled = disabled
if _reset_button != null:
_reset_button.disabled = disabled
for button_value: Variant in _binding_buttons.values():
var button := button_value as Button
if button != null:
button.disabled = disabled
func _reset_mapping() -> void:
if _mapping_manager == null:
return
var reset: bool = _mapping_manager.reset_active_profile()
_progress_label.text = (
"restored the project controller defaults"
if reset else "could not restore controller defaults"
)
_refresh_bindings()
func _refresh_bindings() -> void:
if _mapping_manager == null or _controller_label == null:
return
_controller_label.text = _mapping_manager.get_active_controller_name()
var bindings: Dictionary = _mapping_manager.get_active_bindings()
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
var button := _binding_buttons.get(role) as Button
if button == null:
continue
var raw_binding: Variant = bindings.get(str(role), {})
button.text = (
_mapping_manager.binding_label(raw_binding as Dictionary)
if typeof(raw_binding) == TYPE_DICTIONARY else "unmapped"
)
var conflict_role: StringName = _find_binding_conflict(
role,
raw_binding as Dictionary,
bindings,
)
_apply_binding_button_style(button, not conflict_role.is_empty())
button.tooltip_text = (
"warning: also assigned to "
+ _mapping_manager.get_role_label(conflict_role)
if not conflict_role.is_empty()
else "change " + _mapping_manager.get_role_label(role)
)
_reset_button.disabled = not _mapping_manager.has_custom_mapping()
func _find_binding_conflict(
role: StringName,
binding: Dictionary,
bindings: Dictionary,
) -> StringName:
if binding.is_empty():
return &""
for other_role: StringName in ControllerMappingManagerType.ROLE_ORDER:
if other_role == role:
continue
var raw_other: Variant = bindings.get(str(other_role), {})
if typeof(raw_other) != TYPE_DICTIONARY:
continue
var other := raw_other as Dictionary
if _bindings_conflict(role, binding, other_role, other):
return other_role
return &""
func _bindings_conflict(
first_role: StringName,
first: Dictionary,
second_role: StringName,
second: Dictionary,
) -> bool:
var binding_kind: String = str(first.get("kind", ""))
if binding_kind != str(second.get("kind", "")):
return false
if binding_kind == "button":
return int(first.get("button", -1)) == int(
second.get("button", -1)
)
if binding_kind != "axis":
return false
if int(first.get("axis", -1)) != int(second.get("axis", -1)):
return false
var opposite_trigger_halves: bool = (
first_role in ControllerMappingManagerType.TRIGGER_ROLES
and second_role in ControllerMappingManagerType.TRIGGER_ROLES
and signf(float(first.get("direction", 0.0)))
!= signf(float(second.get("direction", 0.0)))
)
return not opposite_trigger_halves
func _apply_binding_button_style(button: Button, has_conflict: bool) -> void:
UtilityPageStyleType.apply_compact_ocean_button(button)
if not has_conflict:
return
var colors: Dictionary = {
&"normal": UtilityPageStyleType.OCEAN_DANGER,
&"hover": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
&"pressed": UtilityPageStyleType.OCEAN_DANGER.darkened(0.12),
&"focus": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
}
for state: StringName in colors:
var style := UtilityPageStyleType.ocean_button_style(
colors[state] as Color
)
style.content_margin_left = 10.0
style.content_margin_right = 10.0
style.content_margin_top = 4.0
style.content_margin_bottom = 4.0
button.add_theme_stylebox_override(state, style)
func _on_active_controller_changed(_controller_name: String) -> void:
if not _capturing_role.is_empty():
_cancel_capture()
_refresh_bindings()

View file

@ -0,0 +1 @@
uid://b0rl6lgpc0h85

View file

@ -14,11 +14,21 @@ const SIT_SECTOR: int = 0
const RING_RADIUS: float = 172.0
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
var _buttons: Array[BubbleButton] = []
var _is_open: bool = false
var _selected_sector: int = SIT_SECTOR
var _controller_selection_mode: bool = false
var _controller_mapping_manager: ControllerMappingManagerType
func setup_controller_mapping(
mapping_manager: ControllerMappingManagerType,
) -> void:
_controller_mapping_manager = mapping_manager
func _ready() -> void:
@ -92,10 +102,7 @@ func _layout_bubbles() -> void:
func _update_selection() -> void:
var stick: Vector2 = Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
)
var stick: Vector2 = _get_selection_stick()
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
_select_sector_from_offset(stick)
return
@ -104,6 +111,25 @@ func _update_selection() -> void:
_update_mouse_selection()
func _get_selection_stick() -> Vector2:
if (
_controller_mapping_manager != null
and _controller_mapping_manager.has_custom_mapping()
):
return Vector2(
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
),
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
),
)
return Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
)
func _update_mouse_selection() -> void:
var offset: Vector2 = get_local_mouse_position() - size * 0.5
if offset.length() < 24.0:

View file

@ -44,6 +44,9 @@ const SurfaceDrawingToolbarType = preload(
const PlayerSettingsManagerType = preload(
"res://settings/player_settings_manager.gd"
)
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
const WorldWeatherServiceType = preload(
"res://world/world_weather_service.gd"
@ -64,8 +67,15 @@ signal shop_backdrop_visibility_changed(is_visible: bool)
const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse"
const VIRTUAL_MOUSE_TRIGGER_THRESHOLD: float = 0.55
const VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD: float = 0.35
const VIRTUAL_MOUSE_STICK_DEADZONE: float = 0.18
const VIRTUAL_MOUSE_SPEED: float = 720.0
# Android controller mappings may expose LT on its own axis or as the negative
# half of the same signed axis used by RT. Support both without letting the RT
# zoom direction enter virtual-pointer mode.
const VIRTUAL_MOUSE_TRIGGER_AXIS: JoyAxis = JOY_AXIS_TRIGGER_RIGHT
const VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS: JoyAxis = JOY_AXIS_TRIGGER_LEFT
const VIRTUAL_MOUSE_SECONDARY_CLICK_AXIS: JoyAxis = JOY_AXIS_TRIGGER_LEFT
@onready var _status_label: Label = %StatusLabel
@onready var _gameplay_transient_hud: Control = %GameplayTransientHUD
@ -137,7 +147,12 @@ var _virtual_mouse_prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
var _virtual_mouse_window_position: Vector2 = Vector2.ZERO
var _virtual_mouse_button_mask: int = 0
var _virtual_mouse_right_pressed: bool = false
var _injecting_virtual_mouse_event: bool = false
var _virtual_mouse_device_id: int = 0
var _virtual_mouse_trigger_strength: float = 0.0
var _virtual_mouse_stick: Vector2 = Vector2.ZERO
var _virtual_mouse_trigger_rest_by_device: Dictionary[int, float] = {}
var _shared_trigger_rest_by_device: Dictionary[int, float] = {}
var _controller_mapping_manager: ControllerMappingManagerType
func _ready() -> void:
@ -294,21 +309,17 @@ func setup(
set_edge_docks(
settings_manager.current_settings.chat_dock_right,
settings_manager.current_settings.paint_dock_right,
settings_manager.current_settings.chat_mobile_mode,
)
func _input(event: InputEvent) -> void:
if (
_virtual_mouse_active
and _injecting_virtual_mouse_event
and event is InputEventMouseMotion
):
_update_virtual_cursor_position(
(event as InputEventMouseMotion).position
)
if _handle_virtual_mouse_input(event):
get_viewport().set_input_as_handled()
return
if _handle_controller_chat_controls(event):
get_viewport().set_input_as_handled()
return
if (
_surface_drawing_toolbar != null
and _surface_drawing_toolbar.owns_pointer_event(event)
@ -378,6 +389,55 @@ func _input(event: InputEvent) -> void:
get_viewport().set_input_as_handled()
func _handle_controller_chat_controls(event: InputEvent) -> bool:
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
if (
button_event == null
or not button_event.pressed
or not _gameplay_ui_enabled
or _system_menu_open
or _player_menu_open
or _shop_open
):
return false
var use_mapping: bool = (
_controller_mapping_manager != null
and _controller_mapping_manager.has_custom_mapping()
)
var select_pressed: bool = (
_controller_mapping_manager.event_matches_role(
event, ControllerMappingManagerType.ROLE_SELECT
)
if use_mapping
else button_event.button_index == JOY_BUTTON_BACK
)
var focus_pressed: bool = (
_controller_mapping_manager.event_matches_role(
event, ControllerMappingManagerType.ROLE_LB
)
if use_mapping
else button_event.button_index == JOY_BUTTON_LEFT_SHOULDER
)
var accept_pressed: bool = (
_controller_mapping_manager.event_matches_role(
event, ControllerMappingManagerType.ROLE_A
)
if use_mapping
else button_event.button_index == JOY_BUTTON_A
)
if select_pressed:
# Handle Select before focused LineEdit controls consume it. Select owns
# chat visibility while LB only returns input ownership to the world.
_chat_ui.toggle_chat()
return true
if focus_pressed:
_chat_ui.toggle_focus()
return true
if accept_pressed:
return _chat_ui.request_virtual_keyboard()
return false
func _on_emote_selected(emote_id: StringName) -> void:
if emote_id == &"sit" and _player != null:
_player.toggle_sitting()
@ -404,24 +464,59 @@ func _on_quick_action_selected(action_id: StringName) -> void:
func _handle_virtual_mouse_input(event: InputEvent) -> bool:
if (
_controller_mapping_manager != null
and _controller_mapping_manager.has_custom_mapping()
):
return _handle_mapped_virtual_mouse_input(event)
var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
if motion_event != null:
if motion_event.axis == JOY_AXIS_TRIGGER_LEFT:
if motion_event.axis in [
VIRTUAL_MOUSE_TRIGGER_AXIS,
VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS,
]:
if (
_virtual_mouse_active
and motion_event.device != _virtual_mouse_device_id
):
return false
_sample_trigger_rest_values(motion_event.device)
_virtual_mouse_trigger_strength = (
_virtual_mouse_strength_for_device(motion_event.device)
)
var was_active: bool = _virtual_mouse_active
if motion_event.axis_value >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD:
if (
_virtual_mouse_trigger_strength
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
):
if not _virtual_mouse_active and _can_start_virtual_mouse():
_begin_virtual_mouse()
elif _virtual_mouse_active:
_begin_virtual_mouse(motion_event.device)
elif (
_virtual_mouse_active
and _virtual_mouse_trigger_strength
< VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD
):
_end_virtual_mouse()
elif _virtual_mouse_active:
_set_virtual_mouse_button(
MOUSE_BUTTON_RIGHT,
_secondary_click_strength_for_device(
motion_event.device
) >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
)
return was_active or _virtual_mouse_active
if (
_virtual_mouse_active
and motion_event.axis == JOY_AXIS_TRIGGER_RIGHT
and motion_event.device == _virtual_mouse_device_id
and motion_event.axis in [
JOY_AXIS_RIGHT_X,
JOY_AXIS_RIGHT_Y,
]
):
_set_virtual_mouse_button(
MOUSE_BUTTON_RIGHT,
motion_event.axis_value >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
)
if motion_event.axis == JOY_AXIS_RIGHT_X:
_virtual_mouse_stick.x = motion_event.axis_value
else:
_virtual_mouse_stick.y = motion_event.axis_value
return true
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
if (
@ -429,34 +524,124 @@ func _handle_virtual_mouse_input(event: InputEvent) -> bool:
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
and (
_virtual_mouse_active
or Input.get_joy_axis(0, JOY_AXIS_TRIGGER_LEFT)
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
or _virtual_mouse_trigger_strength
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
)
):
if not _virtual_mouse_active and _can_start_virtual_mouse():
_begin_virtual_mouse()
_begin_virtual_mouse(button_event.device)
if _virtual_mouse_active:
_set_virtual_mouse_button(MOUSE_BUTTON_LEFT, button_event.pressed)
return true
return false
func _handle_mapped_virtual_mouse_input(event: InputEvent) -> bool:
var uses_activation: bool = _controller_mapping_manager.event_uses_role(
event,
ControllerMappingManagerType.ROLE_LT,
)
if uses_activation:
var was_active: bool = _virtual_mouse_active
_virtual_mouse_trigger_strength = (
_controller_mapping_manager.get_role_strength(
ControllerMappingManagerType.ROLE_LT
)
)
if (
_virtual_mouse_trigger_strength
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
and not _virtual_mouse_active
and _can_start_virtual_mouse()
):
_begin_virtual_mouse(
_controller_mapping_manager.get_active_device_id()
)
elif (
_virtual_mouse_active
and _virtual_mouse_trigger_strength
< VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD
):
_end_virtual_mouse()
return was_active or _virtual_mouse_active
if not _virtual_mouse_active:
return false
if (
_controller_mapping_manager.event_uses_role(
event,
ControllerMappingManagerType.ROLE_RIGHT_STICK_X,
)
or _controller_mapping_manager.event_uses_role(
event,
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y,
)
):
_virtual_mouse_stick = _mapped_virtual_mouse_stick()
return true
var button_event := event as InputEventJoypadButton
if button_event != null and _controller_mapping_manager.event_uses_role(
event,
ControllerMappingManagerType.ROLE_RB,
):
_set_virtual_mouse_button(MOUSE_BUTTON_LEFT, button_event.pressed)
return true
if _controller_mapping_manager.event_uses_role(
event,
ControllerMappingManagerType.ROLE_RT,
):
_set_virtual_mouse_button(
MOUSE_BUTTON_RIGHT,
_controller_mapping_manager.get_role_strength(
ControllerMappingManagerType.ROLE_RT
) >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
)
return true
return false
func _mapped_virtual_mouse_stick() -> Vector2:
return Vector2(
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
),
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
),
)
func _can_start_virtual_mouse() -> bool:
return (
_gameplay_ui_enabled
and not _showcase_active
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _chat_input_open
and _player != null
and _fishing_spot != null
and _fishing_spot.can_use_surface_drawing()
and _fishing_spot.can_open_system_menu()
and not _emote_radial_menu.is_open()
and not _quick_radial_menu.is_open()
)
func _begin_virtual_mouse() -> void:
func _begin_virtual_mouse(device_id: int) -> void:
if _virtual_mouse_active:
return
_virtual_mouse_active = true
_virtual_mouse_device_id = maxi(device_id, 0)
_virtual_mouse_stick = (
_mapped_virtual_mouse_stick()
if (
_controller_mapping_manager != null
and _controller_mapping_manager.has_custom_mapping()
)
else Vector2(
Input.get_joy_axis(_virtual_mouse_device_id, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(_virtual_mouse_device_id, JOY_AXIS_RIGHT_Y),
)
)
_virtual_mouse_prior_camera_input_enabled = (
_player.is_camera_input_enabled()
)
@ -470,8 +655,11 @@ func _begin_virtual_mouse() -> void:
Input.mouse_mode = Input.MOUSE_MODE_HIDDEN
_virtual_mouse_button_mask = 0
_virtual_mouse_right_pressed = false
_virtual_mouse_window_position = Vector2(get_window().size) * 0.5
_virtual_mouse_window_position = _clamp_virtual_mouse_window_position(
Vector2(get_window().size) * 0.5
)
_controller_virtual_cursor.visible = true
_update_virtual_cursor_position(_virtual_mouse_window_position)
_emit_virtual_mouse_motion(Vector2.ZERO)
@ -483,6 +671,8 @@ func _end_virtual_mouse() -> void:
if _virtual_mouse_right_pressed:
_set_virtual_mouse_button(MOUSE_BUTTON_RIGHT, false)
_virtual_mouse_active = false
_virtual_mouse_trigger_strength = 0.0
_virtual_mouse_stick = Vector2.ZERO
_controller_virtual_cursor.visible = false
if _fishing_spot != null:
_fishing_spot.set_local_menu_input_suppressed(
@ -500,15 +690,12 @@ func _update_virtual_mouse(delta: float) -> void:
if not _virtual_mouse_active:
return
if (
Input.get_joy_axis(0, JOY_AXIS_TRIGGER_LEFT)
< VIRTUAL_MOUSE_TRIGGER_THRESHOLD
_virtual_mouse_trigger_strength
< VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD
):
_end_virtual_mouse()
return
var stick: Vector2 = Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
)
var stick: Vector2 = _virtual_mouse_stick
var stick_length: float = stick.length()
if stick_length <= VIRTUAL_MOUSE_STICK_DEADZONE:
return
@ -526,22 +713,166 @@ func _update_virtual_mouse(delta: float) -> void:
* display_scale
* delta
)
var window_size: Vector2 = Vector2(get_window().size)
_virtual_mouse_window_position = Vector2(
clampf(
_virtual_mouse_window_position.x + relative_motion.x,
0.0,
window_size.x,
),
clampf(
_virtual_mouse_window_position.y + relative_motion.y,
0.0,
window_size.y,
),
_virtual_mouse_window_position = _clamp_virtual_mouse_window_position(
_virtual_mouse_window_position + relative_motion
)
_update_virtual_cursor_position(_virtual_mouse_window_position)
_emit_virtual_mouse_motion(relative_motion)
func _poll_virtual_mouse_controller_state() -> void:
if (
_controller_mapping_manager != null
and _controller_mapping_manager.has_custom_mapping()
):
_poll_mapped_virtual_mouse_controller_state()
return
var device_ids: Array[int] = Input.get_connected_joypads()
# Some Android controller backends deliver device-0 axes without including
# that device in get_connected_joypads(). Player camera input already uses
# this same primary-device fallback.
if device_ids.is_empty():
device_ids.append(0)
for device_id: int in device_ids:
_sample_trigger_rest_values(device_id)
if _virtual_mouse_active:
if not device_ids.has(_virtual_mouse_device_id):
_end_virtual_mouse()
return
_virtual_mouse_trigger_strength = _virtual_mouse_strength_for_device(
_virtual_mouse_device_id
)
_set_virtual_mouse_button(
MOUSE_BUTTON_RIGHT,
_secondary_click_strength_for_device(
_virtual_mouse_device_id
) >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
)
_virtual_mouse_stick = Vector2(
Input.get_joy_axis(
_virtual_mouse_device_id,
JOY_AXIS_RIGHT_X,
),
Input.get_joy_axis(
_virtual_mouse_device_id,
JOY_AXIS_RIGHT_Y,
),
)
return
if not _can_start_virtual_mouse():
return
for device_id: int in device_ids:
var trigger_strength: float = _virtual_mouse_strength_for_device(
device_id
)
if trigger_strength < VIRTUAL_MOUSE_TRIGGER_THRESHOLD:
continue
_virtual_mouse_trigger_strength = trigger_strength
_begin_virtual_mouse(device_id)
return
func _poll_mapped_virtual_mouse_controller_state() -> void:
var trigger_strength: float = _controller_mapping_manager.get_role_strength(
ControllerMappingManagerType.ROLE_LT
)
if _virtual_mouse_active:
_virtual_mouse_trigger_strength = trigger_strength
if trigger_strength < VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD:
_end_virtual_mouse()
return
_virtual_mouse_stick = _mapped_virtual_mouse_stick()
_set_virtual_mouse_button(
MOUSE_BUTTON_RIGHT,
_controller_mapping_manager.get_role_strength(
ControllerMappingManagerType.ROLE_RT
) >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
)
return
if (
trigger_strength >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
and _can_start_virtual_mouse()
):
_virtual_mouse_trigger_strength = trigger_strength
_begin_virtual_mouse(
_controller_mapping_manager.get_active_device_id()
)
func _sample_trigger_rest_values(device_id: int) -> void:
if not _virtual_mouse_trigger_rest_by_device.has(device_id):
_virtual_mouse_trigger_rest_by_device[device_id] = Input.get_joy_axis(
device_id,
VIRTUAL_MOUSE_TRIGGER_AXIS,
)
if not _shared_trigger_rest_by_device.has(device_id):
_shared_trigger_rest_by_device[device_id] = Input.get_joy_axis(
device_id,
VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS,
)
func _virtual_mouse_strength_for_device(device_id: int) -> float:
_sample_trigger_rest_values(device_id)
var dedicated_rest: float = (
_virtual_mouse_trigger_rest_by_device[device_id]
)
var shared_rest: float = _shared_trigger_rest_by_device[device_id]
var dedicated_strength: float = normalized_trigger_strength(
Input.get_joy_axis(device_id, VIRTUAL_MOUSE_TRIGGER_AXIS),
dedicated_rest,
)
var shared_negative_strength: float = directional_trigger_strength(
Input.get_joy_axis(device_id, VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS),
shared_rest,
-1.0,
)
return maxf(dedicated_strength, shared_negative_strength)
func _secondary_click_strength_for_device(device_id: int) -> float:
_sample_trigger_rest_values(device_id)
return directional_trigger_strength(
Input.get_joy_axis(device_id, VIRTUAL_MOUSE_SECONDARY_CLICK_AXIS),
_shared_trigger_rest_by_device[device_id],
1.0,
)
static func normalized_trigger_strength(
axis_value: float,
resting_value: float,
) -> float:
var negative_travel: float = absf(-1.0 - resting_value)
var positive_travel: float = absf(1.0 - resting_value)
var available_travel: float = maxf(negative_travel, positive_travel)
if available_travel <= 0.001:
return 0.0
return clampf(
absf(axis_value - resting_value) / available_travel,
0.0,
1.0,
)
static func directional_trigger_strength(
axis_value: float,
resting_value: float,
direction: float,
) -> float:
var normalized_direction: float = signf(direction)
if is_zero_approx(normalized_direction):
return 0.0
var endpoint: float = normalized_direction
var available_travel: float = absf(endpoint - resting_value)
if available_travel <= 0.001:
return 0.0
var directed_travel: float = (
(axis_value - resting_value) * normalized_direction
)
return clampf(directed_travel / available_travel, 0.0, 1.0)
func _set_virtual_mouse_button(button: MouseButton, pressed: bool) -> void:
var mask: int = (
MOUSE_BUTTON_MASK_LEFT
@ -577,16 +908,40 @@ func _emit_virtual_mouse_motion(relative_motion: Vector2) -> void:
func _parse_virtual_mouse_event(event: InputEventMouse) -> void:
_injecting_virtual_mouse_event = true
Input.parse_input_event(event)
_injecting_virtual_mouse_event = false
func _update_virtual_cursor_position(viewport_position: Vector2) -> void:
var root_transform: Transform2D = _ui_root.get_global_transform_with_canvas()
_controller_virtual_cursor.set_pointer_position(
root_transform.affine_inverse() * viewport_position
var output_scale: float = UIReferencePresentationType.get_scale(
Vector2(get_window().size)
)
_controller_virtual_cursor.set_pointer_position(
viewport_position / output_scale
)
func _clamp_virtual_mouse_window_position(
window_position: Vector2,
) -> Vector2:
var bounds: Rect2 = get_virtual_mouse_window_bounds(
Vector2(get_window().size)
)
return Vector2(
clampf(window_position.x, bounds.position.x, bounds.end.x),
clampf(window_position.y, bounds.position.y, bounds.end.y),
)
static func get_virtual_mouse_window_bounds(window_size: Vector2) -> Rect2:
var output_scale: float = UIReferencePresentationType.get_scale(window_size)
var cursor_margin: Vector2 = (
ControllerVirtualCursorType.CURSOR_SIZE * output_scale * 0.5
)
var bounds_size: Vector2 = Vector2(
maxf(window_size.x - cursor_margin.x * 2.0, 0.0),
maxf(window_size.y - cursor_margin.y * 2.0, 0.0),
)
return Rect2(cursor_margin, bounds_size)
func _toggle_surface_drawing() -> void:
@ -619,7 +974,29 @@ func setup_data_and_identity(
)
func setup_controller_mapping(
mapping_manager: ControllerMappingManagerType,
) -> void:
_controller_mapping_manager = mapping_manager
_player.set_controller_mapping_manager(_controller_mapping_manager)
_emote_radial_menu.setup_controller_mapping(_controller_mapping_manager)
_quick_radial_menu.setup_controller_mapping(_controller_mapping_manager)
_player_menu.setup_controller_mapping(_controller_mapping_manager)
for panel: SettingsPanelType in [
_title_settings_panel, _pause_settings_panel
]:
panel.setup_controller_mapping(_controller_mapping_manager)
func is_controller_mapping_capturing() -> bool:
return (
_title_settings_panel.is_controller_mapping_capturing()
or _pause_settings_panel.is_controller_mapping_capturing()
)
func _process(delta: float) -> void:
_poll_virtual_mouse_controller_state()
_update_virtual_mouse(delta)
_update_experience_bubble_position()
if _item_effects == null or not _gameplay_ui_enabled:
@ -753,8 +1130,13 @@ func set_effective_ui_pixel_size(pixel_size: int) -> void:
_pause_settings_panel.set_effective_ui_pixel_size(pixel_size)
func set_edge_docks(chat_dock_right: bool, paint_dock_right: bool) -> void:
func set_edge_docks(
chat_dock_right: bool,
paint_dock_right: bool,
chat_mobile_mode: bool,
) -> void:
_chat_ui.set_dock_right(chat_dock_right)
_chat_ui.set_mobile_mode(chat_mobile_mode)
_surface_drawing_toolbar.set_dock_right(paint_dock_right)
@ -1131,6 +1513,8 @@ func _update_experience_bubble_position() -> void:
func _on_player_menu_visibility_changed(is_open: bool) -> void:
_player_menu_open = is_open
if is_open:
_end_virtual_mouse()
if is_open and _surface_drawing != null:
_surface_drawing.deactivate()
_gameplay_transient_hud.visible = _gameplay_ui_enabled and not is_open

View file

@ -30,6 +30,9 @@ const PlayerCoolerCapacityType = preload(
const PlayerExperienceType = preload(
"res://progression/player_experience.gd"
)
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const FishBatchSelectionType = preload(
"res://ui/fish_batch_selection.gd"
)
@ -280,6 +283,7 @@ var _network_mail_service: NetworkMailService
var _reservations: PlayerAssetReservationService
var _network_profile_service: NetworkProfileService
var _network_player_list: NetworkPlayerListService
var _controller_mapping_manager: ControllerMappingManagerType
var _default_buyer: FishBuyerProfileType
var _sale_buyer_override: FishBuyerProfileType
var _shop_cooler_context_active: bool = false
@ -564,9 +568,19 @@ func setup(
_refresh_all()
func setup_controller_mapping(
mapping_manager: ControllerMappingManagerType,
) -> void:
_controller_mapping_manager = mapping_manager
_profile_page.setup_controller_mapping(_controller_mapping_manager)
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.echo:
return
if _handle_controller_page_switch(event):
get_viewport().set_input_as_handled()
return
if _handle_direct_page_shortcut(event):
get_viewport().set_input_as_handled()
return
@ -585,6 +599,71 @@ func _input(event: InputEvent) -> void:
get_viewport().set_input_as_handled()
func _handle_controller_page_switch(event: InputEvent) -> bool:
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
var use_mapping: bool = (
_controller_mapping_manager != null
and _controller_mapping_manager.has_custom_mapping()
)
var uses_left_bumper: bool = (
_controller_mapping_manager.event_uses_role(
event, ControllerMappingManagerType.ROLE_LB
)
if use_mapping
else (
button_event != null
and button_event.button_index == JOY_BUTTON_LEFT_SHOULDER
)
)
var uses_right_bumper: bool = (
_controller_mapping_manager.event_uses_role(
event, ControllerMappingManagerType.ROLE_RB
)
if use_mapping
else (
button_event != null
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
)
)
if (
button_event == null
or not (uses_left_bumper or uses_right_bumper)
or not visible
):
return false
if not button_event.pressed:
return true
# Shoulder input belongs to the Player Menu while it is visible, even when
# a transition or modal temporarily prevents changing pages. This keeps LB
# from opening Chat and RB from leaking into gameplay behind the menu.
if (
_transitioning
or _page_transitioning
or _sale_confirmation.visible
or get_viewport().gui_is_dragging()
):
return true
var sections: Array[Section] = [
_last_inventory_section,
Section.LOGBOOK,
Section.NET,
Section.MAIL,
Section.PROFILE,
Section.PLAYERS,
]
var current_index: int = 0
if not _is_inventory_section(_current_section):
current_index = sections.find(_current_section)
if current_index < 0:
current_index = 0
var direction: int = (
-1 if uses_left_bumper else 1
)
var next_index: int = wrapi(current_index + direction, 0, sections.size())
_show_section(sections[next_index])
return true
func _handle_direct_page_shortcut(event: InputEvent) -> bool:
var key_event := event as InputEventKey
if (

View file

@ -2,6 +2,9 @@ class_name ProfilePage
extends Control
const CHECK_DEBOUNCE_SECONDS: float = 0.4
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
var _service: NetworkProfileService
var _experience: PlayerExperience
@ -70,12 +73,20 @@ func setup(
)
func setup_controller_mapping(
mapping_manager: ControllerMappingManagerType,
) -> void:
if _preview != null:
_preview.setup_controller_mapping(mapping_manager)
func activate() -> void:
visible = true
UtilityPageStyle.animate_in(self)
if _service != null:
_load_persisted()
_name_edit.grab_focus()
# Keep controller page switches on the Profile navigation bubble. Focusing
# the name field here summons the Android keyboard during LB/RB traversal.
func deactivate() -> void:

View file

@ -1,6 +1,10 @@
class_name ProfilePreview
extends SubViewportContainer
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
@export_range(0.1, 2.0, 0.05) var drag_sensitivity: float = 0.012
@export_range(0.1, 4.0, 0.1) var keyboard_speed: float = 1.8
@ -8,6 +12,13 @@ extends SubViewportContainer
var _dragging: bool = false
var _visuals: Node3D
var _controller_mapping_manager: ControllerMappingManagerType
func setup_controller_mapping(
mapping_manager: ControllerMappingManagerType,
) -> void:
_controller_mapping_manager = mapping_manager
func _ready() -> void:
@ -36,7 +47,16 @@ func _process(delta: float) -> void:
if not has_focus():
return
var axis := Input.get_axis("ui_left", "ui_right")
var right_stick := Input.get_joy_axis(0, JOY_AXIS_RIGHT_X)
var right_stick: float = (
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
)
if (
_controller_mapping_manager != null
and _controller_mapping_manager.has_custom_mapping()
)
else Input.get_joy_axis(0, JOY_AXIS_RIGHT_X)
)
if absf(right_stick) > 0.2:
axis = right_stick
if absf(axis) > 0.1:

View file

@ -13,6 +13,9 @@ const SECTOR_COUNT: int = 8
const RING_RADIUS: float = 172.0
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const ACTIONS: Array[StringName] = [
&"stuff",
&"logbook",
@ -38,6 +41,13 @@ var _buttons: Array[BubbleButton] = []
var _is_open: bool = false
var _selected_sector: int = 0
var _controller_selection_mode: bool = false
var _controller_mapping_manager: ControllerMappingManagerType
func setup_controller_mapping(
mapping_manager: ControllerMappingManagerType,
) -> void:
_controller_mapping_manager = mapping_manager
func _ready() -> void:
@ -114,10 +124,7 @@ func _layout_bubbles() -> void:
func _update_selection() -> void:
var stick: Vector2 = Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
)
var stick: Vector2 = _get_selection_stick()
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
_select_sector_from_offset(stick)
return
@ -126,6 +133,25 @@ func _update_selection() -> void:
_update_mouse_selection()
func _get_selection_stick() -> Vector2:
if (
_controller_mapping_manager != null
and _controller_mapping_manager.has_custom_mapping()
):
return Vector2(
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
),
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
),
)
return Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
)
func _update_mouse_selection() -> void:
var offset: Vector2 = get_local_mouse_position() - size * 0.5
if offset.length() < 24.0:

View file

@ -18,6 +18,12 @@ const PIXELATION_NAMES: PackedStringArray = [
const SettingsManagerType = preload(
"res://settings/player_settings_manager.gd"
)
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const ControllerMappingPanelType = preload(
"res://ui/controller_mapping_panel.gd"
)
signal applied
signal closed
@ -42,6 +48,7 @@ enum PresentationMode {
@onready var _world_value: BubbleButton = %WorldValue
@onready var _ui_value: BubbleButton = %UIValue
@onready var _chat_dock: BubbleButton = %ChatDock
@onready var _chat_mode: BubbleButton = %ChatMode
@onready var _paint_dock: BubbleButton = %PaintDock
@onready var _mouse_value: BubbleButton = %MouseValue
@onready var _controller_value: BubbleButton = %ControllerValue
@ -84,6 +91,8 @@ var _identity_backups: IdentityBackupService
var _player_identity: PlayerIdentityStore
var _host_identity: HostIdentityStore
var _interface_fonts: InterfaceFontController
var _controller_mapping_manager: ControllerMappingManagerType
var _controller_mapping_panel: ControllerMappingPanelType
var _data_folder_dialog: FileDialog
var _backup_file_dialog: FileDialog
var _export_file_dialog: FileDialog
@ -114,6 +123,7 @@ func _ready() -> void:
%RootBackButton.pressed.connect(close_panel)
%DisplayBackButton.pressed.connect(handle_back)
%ControlsBackButton.pressed.connect(handle_back)
%ControllerMapping.pressed.connect(_open_controller_mapping)
%AccessibilityBackButton.pressed.connect(handle_back)
%DataBackButton.pressed.connect(handle_back)
%RootBackButton.gui_input.connect(_on_back_bubble_gui_input)
@ -136,6 +146,7 @@ func _ready() -> void:
_set_ui_pixelation.bind(index + 1)
)
_chat_dock.pressed.connect(_toggle_chat_dock)
_chat_mode.pressed.connect(_toggle_chat_mode)
_paint_dock.pressed.connect(_toggle_paint_dock)
%MouseDecrease.pressed.connect(_adjust_mouse_sensitivity.bind(-1))
%MouseIncrease.pressed.connect(_adjust_mouse_sensitivity.bind(1))
@ -178,6 +189,11 @@ func _ready() -> void:
parent_control.resized.connect(_refresh_panel_size)
for page: SettingsBubblePage in _pages.values():
page.hide_page()
_controller_mapping_panel = ControllerMappingPanelType.new()
add_child(_controller_mapping_panel)
_controller_mapping_panel.closed.connect(
_on_controller_mapping_panel_closed
)
_style_data_page()
call_deferred("_refresh_panel_size")
@ -190,6 +206,13 @@ func setup_network_profile(
_network_session = session
func setup_controller_mapping(
mapping_manager: ControllerMappingManagerType,
) -> void:
_controller_mapping_manager = mapping_manager
_controller_mapping_panel.setup(_controller_mapping_manager)
func setup_data_and_identity(
data_root: PlayerDataRoot,
identity_backups: IdentityBackupService,
@ -263,6 +286,11 @@ func close_panel(immediate: bool = false) -> void:
func _finish_panel_close(applied_result: bool) -> void:
_cancel_page_transition()
if (
_controller_mapping_panel != null
and _controller_mapping_panel.is_open()
):
_controller_mapping_panel.close_panel()
for page: SettingsBubblePage in _pages.values():
page.hide_page()
_page_stack.clear()
@ -276,6 +304,12 @@ func _finish_panel_close(applied_result: bool) -> void:
func handle_back() -> void:
if (
_controller_mapping_panel != null
and _controller_mapping_panel.is_open()
):
_controller_mapping_panel.request_back()
return
if _page_transition_active:
return
if _page_stack.size() > 1:
@ -284,10 +318,28 @@ func handle_back() -> void:
close_panel()
func _open_controller_mapping() -> void:
if _controller_mapping_manager == null:
_feedback.text = "no controller mapping service is available"
return
_controller_mapping_panel.open_panel()
func _on_controller_mapping_panel_closed() -> void:
%ControllerMapping.grab_focus()
func get_active_page_id() -> StringName:
return _page_stack.back() if not _page_stack.is_empty() else StringName()
func is_controller_mapping_capturing() -> bool:
return (
_controller_mapping_panel != null
and _controller_mapping_panel.is_capturing()
)
func _push_page(page_id: StringName) -> void:
if (
_page_transition_active
@ -689,6 +741,9 @@ func _refresh_value_labels() -> void:
+ _pixel_size_label(settings.ui_pixel_size)
)
_chat_dock.text = "chat dock\n" + ("right" if settings.chat_dock_right else "left")
_chat_mode.text = (
"chat mode\n" + ("mobile" if settings.chat_mobile_mode else "desktop")
)
_paint_dock.text = (
"paint dock\n" + ("right" if settings.paint_dock_right else "left")
)
@ -774,6 +829,17 @@ func _toggle_chat_dock() -> void:
_refresh_value_labels()
func _toggle_chat_mode() -> void:
if _settings_manager == null:
return
var edited: PlayerSettings = _settings_manager.current_settings.copy()
edited.chat_mobile_mode = not edited.chat_mobile_mode
if not _settings_manager.apply_settings(edited):
_feedback.text = "failed to save chat mode setting."
return
_refresh_value_labels()
func _toggle_paint_dock() -> void:
if _settings_manager == null:
return

View file

@ -157,8 +157,8 @@ grow_horizontal = 2
grow_vertical = 2
script = ExtResource("3_page")
page_id = &"display"
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/WorldValue"), NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UIValue"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")])
focus_paths = Array[NodePath]([NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")])
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/WorldValue"), NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UIValue"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/ChatMode"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")])
focus_paths = Array[NodePath]([NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/ChatMode"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")])
initial_focus_path = NodePath("BubbleCluster/WorldRetro")
back_focus_path = NodePath("BubbleCluster/DisplayBackButton")
compact_maximum_layout_size = Vector2(544, 400)
@ -337,36 +337,48 @@ unique_name_in_owner = true
custom_minimum_size = Vector2(0, 0)
text = "chat dock\nleft"
neutral_size = Vector2(96, 96)
desktop_anchor = Vector2(190, 435)
compact_anchor = Vector2(160, 378)
desktop_anchor = Vector2(105, 435)
compact_anchor = Vector2(76, 378)
compact_minimum_size = Vector2(82, 82)
minimum_font_size = 12
maximum_font_size = 16
motion_phase = 1.1
[node name="ChatMode" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 0)
text = "chat mode\ndesktop"
neutral_size = Vector2(96, 96)
desktop_anchor = Vector2(275, 435)
compact_anchor = Vector2(225, 378)
compact_minimum_size = Vector2(82, 82)
minimum_font_size = 12
maximum_font_size = 16
motion_phase = 1.4
[node name="PaintDock" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 0)
text = "paint dock\nright"
neutral_size = Vector2(96, 96)
desktop_anchor = Vector2(530, 435)
compact_anchor = Vector2(448, 378)
desktop_anchor = Vector2(445, 435)
compact_anchor = Vector2(380, 378)
compact_minimum_size = Vector2(82, 82)
minimum_font_size = 12
maximum_font_size = 16
motion_phase = 1.65
motion_phase = 1.7
[node name="DisplayBackButton" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 0)
text = "back"
neutral_size = Vector2(96, 90)
desktop_anchor = Vector2(350, 445)
compact_anchor = Vector2(315, 390)
desktop_anchor = Vector2(615, 435)
compact_anchor = Vector2(530, 378)
compact_minimum_size = Vector2(82, 78)
minimum_font_size = 13
maximum_font_size = 16
motion_phase = 2.2
motion_phase = 2.0
[node name="ControlsPage" type="Control" parent="."]
unique_name_in_owner = true
@ -379,8 +391,8 @@ grow_horizontal = 2
grow_vertical = 2
script = ExtResource("3_page")
page_id = &"controls"
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControlsBackButton")])
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControlsBackButton")])
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
initial_focus_path = NodePath("BubbleCluster/MouseValue")
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
@ -470,24 +482,36 @@ unique_name_in_owner = true
custom_minimum_size = Vector2(0, 0)
text = "invert\nvertical\ncamera\noff"
neutral_size = Vector2(176, 170)
desktop_anchor = Vector2(525, 230)
compact_anchor = Vector2(485, 225)
compact_minimum_size = Vector2(170, 164)
minimum_font_size = 14
maximum_font_size = 24
desktop_anchor = Vector2(525, 145)
compact_anchor = Vector2(485, 135)
motion_phase = 4.5
[node name="ControllerMapping" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 0)
text = "map\ncontroller"
neutral_size = Vector2(160, 150)
desktop_anchor = Vector2(525, 330)
compact_anchor = Vector2(485, 300)
compact_minimum_size = Vector2(148, 138)
minimum_font_size = 14
maximum_font_size = 23
motion_phase = 4.9
[node name="ControlsBackButton" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 0)
text = "back"
neutral_size = Vector2(100, 94)
desktop_anchor = Vector2(525, 425)
compact_anchor = Vector2(540, 395)
desktop_anchor = Vector2(635, 445)
compact_anchor = Vector2(555, 405)
compact_minimum_size = Vector2(84, 80)
minimum_font_size = 14
maximum_font_size = 17
motion_phase = 5.2
motion_phase = 5.5
[node name="AccessibilityPage" type="Control" parent="."]
unique_name_in_owner = true

View file

@ -490,6 +490,11 @@ func _process(delta: float) -> void:
func _input(event: InputEvent) -> void:
if not visible:
return
if (
_settings_panel.visible
and _settings_panel.is_controller_mapping_capturing()
):
return
if _join_game_page.visible:
if event.is_action_pressed("ui_cancel"):
_close_join_game()