feat: complete controller-first menu navigation
This commit is contained in:
parent
26c9308fcf
commit
4ac7d5edb8
35 changed files with 2013 additions and 85 deletions
13
main/main.gd
13
main/main.gd
|
|
@ -380,6 +380,9 @@ func _initialize_after_data_root() -> void:
|
|||
_shop_interaction.local_player_range_changed.connect(
|
||||
_on_shop_range_changed
|
||||
)
|
||||
_game_ui.set_shop_npc_player_in_range(
|
||||
_shop_interaction.is_local_player_in_range()
|
||||
)
|
||||
_save_manager.setup(
|
||||
_player.inventory,
|
||||
_player.collection_log,
|
||||
|
|
@ -1119,6 +1122,9 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
|||
return
|
||||
_apply_world_pixelation(settings.world_pixel_size)
|
||||
_ui_pixelation.set_pixel_size(settings.ui_pixel_size)
|
||||
_ui_pixelation.set_on_screen_keyboard_enabled(
|
||||
settings.on_screen_keyboard_enabled
|
||||
)
|
||||
_game_ui.get_title_screen().set_world_pixelation(
|
||||
settings.world_pixel_size
|
||||
)
|
||||
|
|
@ -1141,7 +1147,11 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
|||
func _apply_world_pixelation(pixel_size: int) -> void:
|
||||
var root_viewport: Viewport = get_viewport()
|
||||
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
|
||||
root_viewport.scaling_3d_scale = 1.0
|
||||
root_viewport.scaling_3d_scale = (
|
||||
0.75
|
||||
if OS.get_environment("NETFISHING_LOW_END") == "1"
|
||||
else 1.0
|
||||
)
|
||||
root_viewport.msaa_3d = Viewport.MSAA_DISABLED
|
||||
root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
|
||||
root_viewport.use_taa = false
|
||||
|
|
@ -1670,6 +1680,7 @@ func _exit_tree() -> void:
|
|||
|
||||
|
||||
func _on_shop_range_changed(in_range: bool) -> void:
|
||||
_game_ui.set_shop_npc_player_in_range(in_range)
|
||||
if not in_range:
|
||||
_game_ui.get_fishing_shop().close_for_range_exit()
|
||||
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const UI_COMPACT_RENDER_HEIGHTS: Array[int] = [0, 408, 336, 264, 192]
|
|||
@export_range(0.001, 0.012, 0.0005) var mouse_camera_sensitivity: float = 0.005
|
||||
@export_range(0.5, 5.0, 0.1) var controller_camera_sensitivity: float = 2.5
|
||||
@export var invert_camera_y: bool = false
|
||||
@export var on_screen_keyboard_enabled: bool = false
|
||||
@export var chat_draft: String = ""
|
||||
@export var chat_collapsed: bool = false
|
||||
@export var chat_dock_right: bool = false
|
||||
|
|
@ -63,6 +64,7 @@ func copy() -> PlayerSettings:
|
|||
result.mouse_camera_sensitivity = mouse_camera_sensitivity
|
||||
result.controller_camera_sensitivity = controller_camera_sensitivity
|
||||
result.invert_camera_y = invert_camera_y
|
||||
result.on_screen_keyboard_enabled = on_screen_keyboard_enabled
|
||||
result.chat_draft = chat_draft
|
||||
result.chat_collapsed = chat_collapsed
|
||||
result.chat_dock_right = chat_dock_right
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ func load_settings() -> bool:
|
|||
if (
|
||||
typeof(accessibility.get("auto_click_enabled")) != TYPE_BOOL
|
||||
or typeof(camera.get("invert_vertical")) != TYPE_BOOL
|
||||
or (
|
||||
accessibility.has("on_screen_keyboard_enabled")
|
||||
and typeof(accessibility["on_screen_keyboard_enabled"]) != TYPE_BOOL
|
||||
)
|
||||
or (
|
||||
accessibility.has("use_readable_interface_font")
|
||||
and typeof(accessibility["use_readable_interface_font"]) != TYPE_BOOL
|
||||
|
|
@ -88,6 +92,9 @@ func load_settings() -> bool:
|
|||
-1.0
|
||||
)
|
||||
loaded.invert_camera_y = camera["invert_vertical"]
|
||||
loaded.on_screen_keyboard_enabled = bool(
|
||||
accessibility.get("on_screen_keyboard_enabled", false)
|
||||
)
|
||||
loaded.world_pixel_size = _read_clamped_integer(
|
||||
presentation.get(
|
||||
"world_pixel_size",
|
||||
|
|
@ -176,6 +183,9 @@ func save_now() -> bool:
|
|||
"auto_click_enabled": current_settings.auto_click_enabled,
|
||||
"auto_click_interval": current_settings.auto_click_interval,
|
||||
"use_readable_interface_font": true,
|
||||
"on_screen_keyboard_enabled": (
|
||||
current_settings.on_screen_keyboard_enabled
|
||||
),
|
||||
},
|
||||
"camera": {
|
||||
"mouse_sensitivity": current_settings.mouse_camera_sensitivity,
|
||||
|
|
|
|||
75
tests/controller_focus_presentation_validation.gd
Normal file
75
tests/controller_focus_presentation_validation.gd
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
extends SceneTree
|
||||
|
||||
const FocusPresentationType = preload(
|
||||
"res://ui/controller_focus_presentation.gd"
|
||||
)
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var stage := Control.new()
|
||||
stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
root.add_child(stage)
|
||||
var presentation := FocusPresentationType.new()
|
||||
stage.add_child(presentation)
|
||||
var standard_button := Button.new()
|
||||
standard_button.text = "standard"
|
||||
standard_button.custom_minimum_size = Vector2(120.0, 60.0)
|
||||
stage.add_child(standard_button)
|
||||
var authored_selector := Button.new()
|
||||
authored_selector.text = "authored selector"
|
||||
authored_selector.position = Vector2(140.0, 0.0)
|
||||
authored_selector.custom_minimum_size = Vector2(160.0, 60.0)
|
||||
authored_selector.set_meta(&"controller_focus_inversion_disabled", true)
|
||||
stage.add_child(authored_selector)
|
||||
await process_frame
|
||||
|
||||
var controller_event := InputEventJoypadButton.new()
|
||||
controller_event.button_index = JOY_BUTTON_A
|
||||
controller_event.pressed = true
|
||||
presentation._input(controller_event)
|
||||
standard_button.grab_focus()
|
||||
await process_frame
|
||||
_expect(
|
||||
standard_button.material != null,
|
||||
"ordinary controller focus receives inversion",
|
||||
)
|
||||
|
||||
authored_selector.grab_focus()
|
||||
await process_frame
|
||||
_expect(
|
||||
standard_button.material == null,
|
||||
"inversion clears when focus moves",
|
||||
)
|
||||
_expect(
|
||||
authored_selector.material == null,
|
||||
"authored selector backgrounds opt out of inversion",
|
||||
)
|
||||
|
||||
standard_button.grab_focus()
|
||||
await process_frame
|
||||
standard_button.focus_mode = Control.FOCUS_NONE
|
||||
await process_frame
|
||||
_expect(
|
||||
standard_button.material == null,
|
||||
"inversion clears when a focused control is defocused",
|
||||
)
|
||||
|
||||
stage.queue_free()
|
||||
if _failures.is_empty():
|
||||
print("Controller focus presentation validation: PASS")
|
||||
quit(0)
|
||||
return
|
||||
for failure: String in _failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
_failures.append(message)
|
||||
1
tests/controller_focus_presentation_validation.gd.uid
Normal file
1
tests/controller_focus_presentation_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bjay8ki3albk6
|
||||
88
tests/controller_focus_recovery_validation.gd
Normal file
88
tests/controller_focus_recovery_validation.gd
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
extends SceneTree
|
||||
|
||||
const ControllerFocusRecoveryType = preload(
|
||||
"res://ui/controller_focus_recovery.gd"
|
||||
)
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var stage := Control.new()
|
||||
stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
root.add_child(stage)
|
||||
var recovery := ControllerFocusRecoveryType.new()
|
||||
stage.add_child(recovery)
|
||||
var option_list := VBoxContainer.new()
|
||||
option_list.position = Vector2(100.0, 100.0)
|
||||
stage.add_child(option_list)
|
||||
var original := _make_button("cute", "cute (show variants)")
|
||||
option_list.add_child(original)
|
||||
await process_frame
|
||||
|
||||
var controller_event := InputEventJoypadButton.new()
|
||||
controller_event.button_index = JOY_BUTTON_A
|
||||
controller_event.pressed = true
|
||||
recovery._input(controller_event)
|
||||
original.grab_focus()
|
||||
await process_frame
|
||||
_expect(root.gui_get_focus_owner() == original, "original option receives focus")
|
||||
|
||||
option_list.remove_child(original)
|
||||
original.queue_free()
|
||||
var replacement := _make_button("cute", "cute (hide variants)")
|
||||
option_list.add_child(replacement)
|
||||
await process_frame
|
||||
await process_frame
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == replacement,
|
||||
"focus follows a rebuilt semantic option",
|
||||
)
|
||||
|
||||
var explicit_target := _make_button("explicit", "explicit")
|
||||
option_list.add_child(explicit_target)
|
||||
option_list.remove_child(replacement)
|
||||
replacement.queue_free()
|
||||
explicit_target.grab_focus()
|
||||
await process_frame
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == explicit_target,
|
||||
"explicit focus changes take precedence over recovery",
|
||||
)
|
||||
var leave_world_ui_event := InputEventJoypadButton.new()
|
||||
leave_world_ui_event.button_index = JOY_BUTTON_LEFT_SHOULDER
|
||||
leave_world_ui_event.pressed = true
|
||||
recovery._input(leave_world_ui_event)
|
||||
root.gui_release_focus()
|
||||
await process_frame
|
||||
await process_frame
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == null,
|
||||
"LB intentionally leaving world UI is never recovered",
|
||||
)
|
||||
|
||||
stage.queue_free()
|
||||
if _failures.is_empty():
|
||||
print("Controller focus recovery validation: PASS")
|
||||
quit(0)
|
||||
return
|
||||
for failure: String in _failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _make_button(text: String, tooltip: String) -> Button:
|
||||
var button := Button.new()
|
||||
button.text = text
|
||||
button.tooltip_text = tooltip
|
||||
button.custom_minimum_size = Vector2(120.0, 48.0)
|
||||
return button
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
_failures.append(message)
|
||||
1
tests/controller_focus_recovery_validation.gd.uid
Normal file
1
tests/controller_focus_recovery_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cqk75fxykqrhs
|
||||
82
tests/controller_ui_navigation_validation.gd
Normal file
82
tests/controller_ui_navigation_validation.gd
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
extends SceneTree
|
||||
|
||||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
const UIReferencePresentationType = preload(
|
||||
"res://ui/ui_reference_presentation.gd"
|
||||
)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_validate_spatial_navigation()
|
||||
_validate_controller_hierarchy_contract()
|
||||
_validate_four_by_three_centering()
|
||||
_validate_low_end_profile_contract()
|
||||
print("Controller UI navigation validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _validate_spatial_navigation() -> void:
|
||||
var host := Control.new()
|
||||
root.add_child(host)
|
||||
var center := _make_button(host, "center", Vector2(200.0, 200.0))
|
||||
var left := _make_button(host, "left", Vector2(50.0, 200.0))
|
||||
var right := _make_button(host, "right", Vector2(350.0, 200.0))
|
||||
var top := _make_button(host, "top", Vector2(200.0, 50.0))
|
||||
var bottom := _make_button(host, "bottom", Vector2(200.0, 350.0))
|
||||
var controls: Array[Control] = [center, left, right, top, bottom]
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
|
||||
assert(center.get_node(center.focus_neighbor_left) == left)
|
||||
assert(center.get_node(center.focus_neighbor_right) == right)
|
||||
assert(center.get_node(center.focus_neighbor_top) == top)
|
||||
assert(center.get_node(center.focus_neighbor_bottom) == bottom)
|
||||
host.queue_free()
|
||||
|
||||
|
||||
func _validate_controller_hierarchy_contract() -> void:
|
||||
var source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/player_menu.gd"
|
||||
)
|
||||
assert(source.contains("ROLE_POINTER_MODIFIER"))
|
||||
assert(source.contains("ROLE_CAMERA_ZOOM"))
|
||||
assert(source.contains("_handle_controller_secondary_switch"))
|
||||
assert(source.contains("ROLE_LB"))
|
||||
assert(source.contains("ROLE_RB"))
|
||||
assert(source.contains("_reserve_main_navigation_for_page_switching"))
|
||||
|
||||
|
||||
func _validate_four_by_three_centering() -> void:
|
||||
var stage_position: Vector2 = (
|
||||
UIReferencePresentationType.get_stage_position(Vector2(640.0, 480.0))
|
||||
)
|
||||
assert(stage_position.is_equal_approx(Vector2(0.0, 120.0)))
|
||||
|
||||
|
||||
func _validate_low_end_profile_contract() -> void:
|
||||
var launcher: String = FileAccess.get_file_as_string(
|
||||
"res://scripts/portmaster/NETfishing.sh"
|
||||
)
|
||||
assert(launcher.contains("allwinner,h616"))
|
||||
assert(launcher.contains("sun50iw9p1"))
|
||||
assert(launcher.contains("NETFISHING_LOW_END=1"))
|
||||
assert(launcher.contains("--max-fps 30"))
|
||||
assert(launcher.contains("--audio-output-latency 40"))
|
||||
|
||||
|
||||
func _make_button(
|
||||
host: Control,
|
||||
button_name: String,
|
||||
button_position: Vector2,
|
||||
) -> Button:
|
||||
var button := Button.new()
|
||||
button.name = button_name
|
||||
button.position = button_position
|
||||
button.size = Vector2(80.0, 80.0)
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
host.add_child(button)
|
||||
return button
|
||||
1
tests/controller_ui_navigation_validation.gd.uid
Normal file
1
tests/controller_ui_navigation_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cbv0ia42rqu41
|
||||
82
tests/on_screen_keyboard_validation.gd
Normal file
82
tests/on_screen_keyboard_validation.gd
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
extends SceneTree
|
||||
|
||||
const KeyboardType = preload("res://ui/on_screen_keyboard.gd")
|
||||
const SettingsManagerType = preload(
|
||||
"res://settings/player_settings_manager.gd"
|
||||
)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_validate_default_and_persistence()
|
||||
await _validate_keyboard_entry()
|
||||
print("On-screen keyboard validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _validate_default_and_persistence() -> void:
|
||||
var defaults := PlayerSettings.new()
|
||||
assert(not defaults.on_screen_keyboard_enabled)
|
||||
var manager := SettingsManagerType.new()
|
||||
root.add_child(manager)
|
||||
assert(manager.load_settings())
|
||||
var edited: PlayerSettings = manager.current_settings.copy()
|
||||
edited.on_screen_keyboard_enabled = true
|
||||
assert(manager.apply_settings(edited))
|
||||
var reloaded := SettingsManagerType.new()
|
||||
root.add_child(reloaded)
|
||||
assert(reloaded.load_settings())
|
||||
assert(reloaded.current_settings.on_screen_keyboard_enabled)
|
||||
manager.queue_free()
|
||||
reloaded.queue_free()
|
||||
|
||||
|
||||
func _validate_keyboard_entry() -> void:
|
||||
var host := Control.new()
|
||||
root.add_child(host)
|
||||
var edit := LineEdit.new()
|
||||
edit.focus_mode = Control.FOCUS_ALL
|
||||
host.add_child(edit)
|
||||
var keyboard := KeyboardType.new()
|
||||
host.add_child(keyboard)
|
||||
await process_frame
|
||||
keyboard.set_enabled(true)
|
||||
edit.grab_focus()
|
||||
var activate_event := InputEventJoypadButton.new()
|
||||
activate_event.button_index = JOY_BUTTON_A
|
||||
activate_event.pressed = true
|
||||
keyboard.call("_input", activate_event)
|
||||
assert(keyboard.is_open())
|
||||
keyboard.call("_type_character", "a")
|
||||
keyboard.call("_type_space")
|
||||
keyboard.call("_set_page", KeyboardType.Page.UPPER)
|
||||
keyboard.call("_type_character", "B")
|
||||
assert(edit.text == "a B")
|
||||
keyboard.call("_move_caret", -1)
|
||||
keyboard.call("_type_character", "C")
|
||||
assert(edit.text == "a CB")
|
||||
var backspace_event := InputEventJoypadButton.new()
|
||||
backspace_event.button_index = JOY_BUTTON_X
|
||||
backspace_event.pressed = true
|
||||
keyboard.call("_input", backspace_event)
|
||||
assert(edit.text == "a B")
|
||||
var defocus_event := InputEventJoypadButton.new()
|
||||
defocus_event.button_index = JOY_BUTTON_LEFT_SHOULDER
|
||||
defocus_event.pressed = true
|
||||
keyboard.call("_input", defocus_event)
|
||||
assert(not keyboard.is_open())
|
||||
assert(root.gui_get_focus_owner() == null)
|
||||
edit.grab_focus()
|
||||
keyboard.call("_input", activate_event)
|
||||
assert(keyboard.is_open())
|
||||
var submitted: Array[String] = []
|
||||
edit.text_submitted.connect(func(value: String) -> void:
|
||||
submitted.append(value)
|
||||
)
|
||||
keyboard.call("_submit")
|
||||
assert(not keyboard.is_open())
|
||||
assert(submitted == ["a B"])
|
||||
host.queue_free()
|
||||
1
tests/on_screen_keyboard_validation.gd.uid
Normal file
1
tests/on_screen_keyboard_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bgmt6joqdk37q
|
||||
|
|
@ -94,7 +94,10 @@ func _run() -> void:
|
|||
assert(player_menu.size.is_equal_approx(
|
||||
UIReferencePresentationType.REFERENCE_SIZE
|
||||
))
|
||||
assert(hotbar.position.is_equal_approx(Vector2.ZERO))
|
||||
assert(hotbar.position.is_equal_approx(Vector2(
|
||||
0.0,
|
||||
canonical_stage.position.y,
|
||||
)))
|
||||
assert(hotbar.size.is_equal_approx(
|
||||
UIReferencePresentationType.REFERENCE_SIZE
|
||||
))
|
||||
|
|
|
|||
|
|
@ -568,6 +568,21 @@ func _refresh_input_ownership() -> void:
|
|||
)
|
||||
_panel.mouse_filter = filter
|
||||
_history.mouse_filter = filter
|
||||
_entry.focus_mode = (
|
||||
Control.FOCUS_ALL if _opened else Control.FOCUS_NONE
|
||||
)
|
||||
_collapse_button.focus_mode = (
|
||||
Control.FOCUS_ALL if _opened else Control.FOCUS_NONE
|
||||
)
|
||||
_height_button.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if _opened and not _mobile_mode
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
if not _opened:
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if focus_owner in [_entry, _collapse_button, _height_button]:
|
||||
get_viewport().gui_release_focus()
|
||||
|
||||
|
||||
func _clock_panel_style() -> StyleBoxFlat:
|
||||
|
|
@ -886,9 +901,14 @@ func _refresh_visibility() -> void:
|
|||
_collapse_button.show()
|
||||
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
||||
_panel.show()
|
||||
_collapse_button.focus_mode = (
|
||||
Control.FOCUS_ALL if _opened else Control.FOCUS_NONE
|
||||
)
|
||||
_height_button.visible = not _mobile_mode
|
||||
_height_button.focus_mode = (
|
||||
Control.FOCUS_NONE if _mobile_mode else Control.FOCUS_ALL
|
||||
Control.FOCUS_ALL
|
||||
if _opened and not _mobile_mode
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
_unread_indicator.visible = collapsed and _collapsed_has_unread
|
||||
_hint.visible = not _opened
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ var _base_position: Vector2 = Vector2.ZERO
|
|||
var _presented_size: Vector2 = Vector2.ZERO
|
||||
var _compact: bool = false
|
||||
var _presentation_initialized: bool = false
|
||||
var _controller_preview_active: bool = false
|
||||
var _controller_preview_texture: Texture2D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -146,6 +148,15 @@ func set_drag_enabled(enabled: bool) -> void:
|
|||
_hovered = false
|
||||
|
||||
|
||||
func set_controller_placement_preview(
|
||||
active: bool,
|
||||
texture: Texture2D,
|
||||
) -> void:
|
||||
_controller_preview_active = active
|
||||
_controller_preview_texture = texture
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
if _hotbar == null:
|
||||
return
|
||||
|
|
@ -161,11 +172,16 @@ func refresh() -> void:
|
|||
if _fish_inventory != null and not catch_id.is_empty()
|
||||
else null
|
||||
)
|
||||
_item_icon.texture = (
|
||||
var assigned_texture: Texture2D = (
|
||||
fish_catch.fish.display_texture
|
||||
if fish_catch != null
|
||||
else item.icon if item != null else null
|
||||
)
|
||||
_item_icon.texture = (
|
||||
_controller_preview_texture
|
||||
if _controller_preview_active
|
||||
else assigned_texture
|
||||
)
|
||||
var quantity: int = (
|
||||
_bag.get_quantity(item_id)
|
||||
if _bag != null and not item_id.is_empty()
|
||||
|
|
@ -177,7 +193,9 @@ func refresh() -> void:
|
|||
else ""
|
||||
)
|
||||
_quantity_label.text = quantity_text
|
||||
_quantity_label.visible = not quantity_text.is_empty()
|
||||
_quantity_label.visible = (
|
||||
not _controller_preview_active and not quantity_text.is_empty()
|
||||
)
|
||||
tooltip_text = (
|
||||
"%s · %.1f lb" % [
|
||||
FishQualityType.qualified_name(
|
||||
|
|
@ -192,7 +210,11 @@ func refresh() -> void:
|
|||
var was_selected: bool = _selected
|
||||
var was_empty: bool = _empty
|
||||
_selected = slot_index == _hotbar.get_selected_slot()
|
||||
_empty = item == null and fish_catch == null
|
||||
_empty = (
|
||||
not _controller_preview_active
|
||||
and item == null
|
||||
and fish_catch == null
|
||||
)
|
||||
if was_selected != _selected or was_empty != _empty:
|
||||
_apply_style()
|
||||
|
||||
|
|
@ -220,7 +242,9 @@ func _apply_style() -> void:
|
|||
var normal_fill: Color = profile.normal_fill
|
||||
normal_fill.a = 1.0
|
||||
var selected_fill: Color = normal_fill
|
||||
if _selected:
|
||||
if _controller_preview_active:
|
||||
selected_fill = normal_fill.lightened(0.22)
|
||||
elif _selected:
|
||||
selected_fill = normal_fill.lightened(0.12)
|
||||
add_theme_stylebox_override(
|
||||
"normal",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,53 @@ func _ready() -> void:
|
|||
_apply_icon_presentation(neutral_size)
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if _adjustment_direction(self) == 0:
|
||||
return
|
||||
var direction: int = 0
|
||||
if event.is_action_pressed(&"ui_left"):
|
||||
direction = -1
|
||||
elif event.is_action_pressed(&"ui_right"):
|
||||
direction = 1
|
||||
if direction == 0:
|
||||
return
|
||||
var adjustment_button: BaseButton = _find_adjustment_button(direction)
|
||||
if adjustment_button == null:
|
||||
return
|
||||
adjustment_button.pressed.emit()
|
||||
accept_event()
|
||||
|
||||
|
||||
func _find_adjustment_button(direction: int) -> BaseButton:
|
||||
var parent_node: Node = get_parent()
|
||||
if parent_node == null:
|
||||
return null
|
||||
for child: Node in parent_node.get_children():
|
||||
var button := child as BaseButton
|
||||
if (
|
||||
button != null
|
||||
and button.visible
|
||||
and not button.disabled
|
||||
and _adjustment_direction(button) == direction
|
||||
):
|
||||
return button
|
||||
return null
|
||||
|
||||
|
||||
func _adjustment_direction(button: BaseButton) -> int:
|
||||
var descriptions: Array[String] = [
|
||||
button.text.strip_edges().to_lower(),
|
||||
button.tooltip_text.strip_edges().to_lower(),
|
||||
button.accessibility_name.strip_edges().to_lower(),
|
||||
]
|
||||
for description: String in descriptions:
|
||||
if description in ["-", "−", "minus", "decrease"]:
|
||||
return -1
|
||||
if description in ["+", "plus", "increase"]:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
func apply_layout(
|
||||
center: Vector2,
|
||||
bubble_size: Vector2,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
class_name BubbleCluster
|
||||
extends Control
|
||||
|
||||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
|
||||
@export var profile: BubbleMenuProfile
|
||||
@export var desktop_reference_size: Vector2 = Vector2(396.0, 318.0)
|
||||
@export var compact_reference_size: Vector2 = Vector2(294.0, 200.0)
|
||||
|
|
@ -18,14 +22,6 @@ func configure(bubbles: Array[BubbleButton]) -> void:
|
|||
if bubble.profile == null:
|
||||
bubble.profile = profile
|
||||
bubble.apply_profile()
|
||||
if index > 0:
|
||||
bubble.focus_neighbor_top = bubble.get_path_to(
|
||||
_bubbles[index - 1]
|
||||
)
|
||||
if index + 1 < _bubbles.size():
|
||||
bubble.focus_neighbor_bottom = bubble.get_path_to(
|
||||
_bubbles[index + 1]
|
||||
)
|
||||
|
||||
|
||||
func apply_layout(field_size: Vector2, compact: bool) -> void:
|
||||
|
|
@ -52,6 +48,7 @@ func apply_layout(field_size: Vector2, compact: bool) -> void:
|
|||
bubble_size,
|
||||
profile.font_size_ratio
|
||||
)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_bubbles)
|
||||
|
||||
|
||||
func advance_motion(delta: float) -> void:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ var _quality_color := Color.WHITE
|
|||
|
||||
|
||||
func _ready() -> void:
|
||||
set_meta(&"controller_focus_inversion_disabled", true)
|
||||
resized.connect(_update_visual_pivot)
|
||||
focus_entered.connect(_refresh_style)
|
||||
focus_exited.connect(_refresh_style)
|
||||
|
|
|
|||
57
ui/controller_focus_navigation.gd
Normal file
57
ui/controller_focus_navigation.gd
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
class_name ControllerFocusNavigation
|
||||
extends RefCounted
|
||||
|
||||
const PERPENDICULAR_WEIGHT: float = 2.5
|
||||
const MINIMUM_FORWARD_DISTANCE: float = 0.5
|
||||
|
||||
|
||||
static func configure_spatial_neighbors(controls: Array) -> void:
|
||||
var candidates: Array[Control] = []
|
||||
for item: Variant in controls:
|
||||
var control := item as Control
|
||||
if control == null or not control.is_visible_in_tree():
|
||||
continue
|
||||
candidates.append(control)
|
||||
for control: Control in candidates:
|
||||
control.focus_neighbor_left = _neighbor_path(
|
||||
control, candidates, Vector2.LEFT
|
||||
)
|
||||
control.focus_neighbor_right = _neighbor_path(
|
||||
control, candidates, Vector2.RIGHT
|
||||
)
|
||||
control.focus_neighbor_top = _neighbor_path(
|
||||
control, candidates, Vector2.UP
|
||||
)
|
||||
control.focus_neighbor_bottom = _neighbor_path(
|
||||
control, candidates, Vector2.DOWN
|
||||
)
|
||||
|
||||
|
||||
static func _neighbor_path(
|
||||
origin: Control,
|
||||
candidates: Array[Control],
|
||||
direction: Vector2,
|
||||
) -> NodePath:
|
||||
var origin_center: Vector2 = origin.get_global_rect().get_center()
|
||||
var best: Control = null
|
||||
var best_score: float = INF
|
||||
for candidate: Control in candidates:
|
||||
if candidate == origin or candidate.focus_mode == Control.FOCUS_NONE:
|
||||
continue
|
||||
var delta: Vector2 = (
|
||||
candidate.get_global_rect().get_center() - origin_center
|
||||
)
|
||||
var forward_distance: float = delta.dot(direction)
|
||||
if forward_distance <= MINIMUM_FORWARD_DISTANCE:
|
||||
continue
|
||||
var perpendicular_distance: float = absf(delta.cross(direction))
|
||||
var score: float = (
|
||||
forward_distance
|
||||
+ perpendicular_distance * PERPENDICULAR_WEIGHT
|
||||
)
|
||||
if score < best_score:
|
||||
best = candidate
|
||||
best_score = score
|
||||
if best == null:
|
||||
return NodePath()
|
||||
return origin.get_path_to(best)
|
||||
1
ui/controller_focus_navigation.gd.uid
Normal file
1
ui/controller_focus_navigation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dfb6bobetmujn
|
||||
97
ui/controller_focus_presentation.gd
Normal file
97
ui/controller_focus_presentation.gd
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
class_name ControllerFocusPresentation
|
||||
extends Node
|
||||
|
||||
const CONTROLLER_MOTION_THRESHOLD: float = 0.35
|
||||
const INVERSION_DISABLED_META: StringName = &"controller_focus_inversion_disabled"
|
||||
|
||||
var _controller_active: bool = false
|
||||
var _focused_item: CanvasItem
|
||||
var _original_material: Material
|
||||
var _inversion_material: ShaderMaterial
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var inversion_shader := Shader.new()
|
||||
inversion_shader.code = """
|
||||
shader_type canvas_item;
|
||||
render_mode unshaded;
|
||||
|
||||
void fragment() {
|
||||
vec4 source = texture(TEXTURE, UV) * COLOR;
|
||||
COLOR = vec4(vec3(1.0) - source.rgb, source.a);
|
||||
}
|
||||
"""
|
||||
_inversion_material = ShaderMaterial.new()
|
||||
_inversion_material.shader = inversion_shader
|
||||
get_viewport().gui_focus_changed.connect(_on_focus_changed)
|
||||
set_process_input(true)
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_restore_focused_item()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventJoypadButton:
|
||||
if (event as InputEventJoypadButton).pressed:
|
||||
_set_controller_active(true)
|
||||
elif event is InputEventJoypadMotion:
|
||||
if absf((event as InputEventJoypadMotion).axis_value) >= (
|
||||
CONTROLLER_MOTION_THRESHOLD
|
||||
):
|
||||
_set_controller_active(true)
|
||||
elif event is InputEventMouseButton:
|
||||
if (event as InputEventMouseButton).pressed:
|
||||
_set_controller_active(false)
|
||||
elif event is InputEventKey:
|
||||
if (event as InputEventKey).pressed:
|
||||
_set_controller_active(false)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not is_instance_valid(_focused_item):
|
||||
_focused_item = null
|
||||
_original_material = null
|
||||
return
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if (
|
||||
focus_owner != _focused_item
|
||||
or not focus_owner.is_visible_in_tree()
|
||||
or focus_owner.focus_mode == Control.FOCUS_NONE
|
||||
or bool(focus_owner.get_meta(INVERSION_DISABLED_META, false))
|
||||
):
|
||||
_apply_to_focus(focus_owner)
|
||||
|
||||
|
||||
func _set_controller_active(active: bool) -> void:
|
||||
if _controller_active == active:
|
||||
return
|
||||
_controller_active = active
|
||||
_apply_to_focus(get_viewport().gui_get_focus_owner())
|
||||
|
||||
|
||||
func _on_focus_changed(control: Control) -> void:
|
||||
_apply_to_focus(control)
|
||||
|
||||
|
||||
func _apply_to_focus(control: Control) -> void:
|
||||
_restore_focused_item()
|
||||
if (
|
||||
not _controller_active
|
||||
or control == null
|
||||
or not control.is_visible_in_tree()
|
||||
or control.focus_mode == Control.FOCUS_NONE
|
||||
or bool(control.get_meta(INVERSION_DISABLED_META, false))
|
||||
):
|
||||
return
|
||||
_focused_item = control
|
||||
_original_material = _focused_item.material
|
||||
_focused_item.material = _inversion_material
|
||||
|
||||
|
||||
func _restore_focused_item() -> void:
|
||||
if is_instance_valid(_focused_item):
|
||||
_focused_item.material = _original_material
|
||||
_focused_item = null
|
||||
_original_material = null
|
||||
1
ui/controller_focus_presentation.gd.uid
Normal file
1
ui/controller_focus_presentation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b2ccvcx1rfd1x
|
||||
142
ui/controller_focus_recovery.gd
Normal file
142
ui/controller_focus_recovery.gd
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
class_name ControllerFocusRecovery
|
||||
extends Node
|
||||
|
||||
const CONTROLLER_AXIS_THRESHOLD: float = 0.35
|
||||
const SEMANTIC_MATCH_BONUS: float = 1000000.0
|
||||
|
||||
var _controller_active: bool = false
|
||||
var _last_focus_center: Vector2 = Vector2.ZERO
|
||||
var _last_focus_key: String = ""
|
||||
var _scope_chain: Array[WeakRef] = []
|
||||
var _recovery_generation: int = 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_process_input(true)
|
||||
set_process(true)
|
||||
get_viewport().gui_focus_changed.connect(_on_gui_focus_changed)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var viewport := get_viewport()
|
||||
if viewport.gui_focus_changed.is_connected(_on_gui_focus_changed):
|
||||
viewport.gui_focus_changed.disconnect(_on_gui_focus_changed)
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventJoypadButton:
|
||||
var button_event := event as InputEventJoypadButton
|
||||
if button_event.pressed:
|
||||
_controller_active = true
|
||||
if button_event.button_index == JOY_BUTTON_LEFT_SHOULDER:
|
||||
_recovery_generation += 1
|
||||
_scope_chain.clear()
|
||||
return
|
||||
if event is InputEventJoypadMotion:
|
||||
if absf((event as InputEventJoypadMotion).axis_value) >= (
|
||||
CONTROLLER_AXIS_THRESHOLD
|
||||
):
|
||||
_controller_active = true
|
||||
return
|
||||
if event is InputEventMouseButton:
|
||||
if (event as InputEventMouseButton).pressed:
|
||||
_controller_active = false
|
||||
return
|
||||
if event is InputEventKey and (event as InputEventKey).pressed:
|
||||
_controller_active = false
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if (
|
||||
_controller_active
|
||||
and not _scope_chain.is_empty()
|
||||
and get_viewport().gui_get_focus_owner() == null
|
||||
):
|
||||
_recover_focus(_recovery_generation)
|
||||
|
||||
|
||||
func _on_gui_focus_changed(control: Control) -> void:
|
||||
_recovery_generation += 1
|
||||
if control != null:
|
||||
if _controller_active and control is BaseButton:
|
||||
_remember_focus(control)
|
||||
return
|
||||
if not _controller_active or _scope_chain.is_empty():
|
||||
return
|
||||
var generation: int = _recovery_generation
|
||||
_recover_focus.call_deferred(generation)
|
||||
|
||||
|
||||
func _remember_focus(control: Control) -> void:
|
||||
_last_focus_center = control.get_global_rect().get_center()
|
||||
_last_focus_key = _semantic_key(control)
|
||||
_scope_chain.clear()
|
||||
var recovery_root: Node = get_parent()
|
||||
var ancestor: Node = control.get_parent()
|
||||
while ancestor != null:
|
||||
if ancestor is Control:
|
||||
_scope_chain.append(weakref(ancestor))
|
||||
if ancestor == recovery_root:
|
||||
break
|
||||
ancestor = ancestor.get_parent()
|
||||
|
||||
|
||||
func _recover_focus(generation: int) -> void:
|
||||
if generation != _recovery_generation:
|
||||
return
|
||||
if not _controller_active or get_viewport().gui_get_focus_owner() != null:
|
||||
return
|
||||
for scope_reference: WeakRef in _scope_chain:
|
||||
var scope := scope_reference.get_ref() as Control
|
||||
if (
|
||||
scope == null
|
||||
or not is_instance_valid(scope)
|
||||
or not scope.is_inside_tree()
|
||||
or not scope.is_visible_in_tree()
|
||||
):
|
||||
continue
|
||||
var replacement := _best_replacement_in(scope)
|
||||
if replacement != null:
|
||||
replacement.grab_focus()
|
||||
return
|
||||
|
||||
|
||||
func _best_replacement_in(scope: Control) -> Control:
|
||||
var best: Control = null
|
||||
var best_score: float = INF
|
||||
for node: Node in scope.find_children("*", "Control", true, false):
|
||||
var candidate := node as Control
|
||||
if not _is_focusable(candidate):
|
||||
continue
|
||||
var distance: float = candidate.get_global_rect().get_center().distance_squared_to(
|
||||
_last_focus_center
|
||||
)
|
||||
if not _last_focus_key.is_empty() and _semantic_key(candidate) == _last_focus_key:
|
||||
distance -= SEMANTIC_MATCH_BONUS
|
||||
if distance < best_score:
|
||||
best = candidate
|
||||
best_score = distance
|
||||
return best
|
||||
|
||||
|
||||
func _is_focusable(control: Control) -> bool:
|
||||
if (
|
||||
control == null
|
||||
or not control.is_inside_tree()
|
||||
or not control.is_visible_in_tree()
|
||||
or control.focus_mode == Control.FOCUS_NONE
|
||||
):
|
||||
return false
|
||||
var button := control as BaseButton
|
||||
return button == null or not button.disabled
|
||||
|
||||
|
||||
func _semantic_key(control: Control) -> String:
|
||||
if control.has_meta(&"controller_focus_key"):
|
||||
return str(control.get_meta(&"controller_focus_key"))
|
||||
var button := control as Button
|
||||
var label: String = button.text if button != null else ""
|
||||
var tooltip: String = control.tooltip_text
|
||||
tooltip = tooltip.trim_suffix(" (show variants)")
|
||||
tooltip = tooltip.trim_suffix(" (hide variants)")
|
||||
return "%s|%s|%s" % [control.get_class(), label, tooltip]
|
||||
1
ui/controller_focus_recovery.gd.uid
Normal file
1
ui/controller_focus_recovery.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c7cw61ethr3pt
|
||||
|
|
@ -133,6 +133,8 @@ const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0
|
|||
@onready var _shop_prompt_key: Label = %ShopPromptKey
|
||||
@onready var _shop_prompt_pointer: Polygon2D = %ShopPromptPointer
|
||||
var _shop_animalese_voice: AnimaleseVoiceType
|
||||
var _shop_npc_player_in_range: bool = false
|
||||
var _shop_npc_spoken_for_current_visit: bool = false
|
||||
@onready var _effect_status: Label = %EffectStatus
|
||||
@onready var _chat_ui: ChatUIType = %ChatUI
|
||||
@onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu
|
||||
|
|
@ -207,6 +209,12 @@ func _ready() -> void:
|
|||
_hotbar_ui.presentation_transition_finished.connect(
|
||||
_on_hotbar_presentation_transition_finished
|
||||
)
|
||||
_player_menu.controller_hotbar_placement_requested.connect(
|
||||
_hotbar_ui.begin_controller_placement
|
||||
)
|
||||
_player_menu.controller_hotbar_placement_ended.connect(
|
||||
_hotbar_ui.end_controller_placement
|
||||
)
|
||||
_title_settings_panel.panel_visibility_changed.connect(
|
||||
_on_settings_visibility_changed
|
||||
)
|
||||
|
|
@ -503,7 +511,7 @@ func _handle_controller_chat_controls(event: InputEvent) -> bool:
|
|||
_chat_ui.toggle_chat()
|
||||
return true
|
||||
if focus_pressed:
|
||||
_chat_ui.toggle_focus()
|
||||
_chat_ui.refocus_gameplay()
|
||||
return true
|
||||
if accept_pressed:
|
||||
return _chat_ui.request_virtual_keyboard()
|
||||
|
|
@ -1302,6 +1310,8 @@ func _on_hud_bait_inventory_changed() -> void:
|
|||
|
||||
func set_gameplay_ui_enabled(enabled: bool) -> void:
|
||||
_gameplay_ui_enabled = enabled
|
||||
if enabled:
|
||||
_try_start_shop_npc_speech()
|
||||
_gameplay_transient_hud.visible = enabled and not _player_menu_open
|
||||
_experience_presentation.visible = enabled
|
||||
_refresh_chat_availability()
|
||||
|
|
@ -1362,7 +1372,6 @@ func set_shop_prompt_visible(
|
|||
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
|
||||
) -> void:
|
||||
_apply_shop_prompt_style()
|
||||
var was_visible := _shop_prompt.visible
|
||||
_shop_prompt.visible = (
|
||||
is_visible
|
||||
and _gameplay_ui_enabled
|
||||
|
|
@ -1371,7 +1380,28 @@ func set_shop_prompt_visible(
|
|||
and not _shop_open
|
||||
)
|
||||
if _shop_prompt.visible:
|
||||
if not was_visible:
|
||||
if world_anchor.is_finite():
|
||||
_position_shop_prompt(world_anchor)
|
||||
|
||||
|
||||
func set_shop_npc_player_in_range(in_range: bool) -> void:
|
||||
if _shop_npc_player_in_range == in_range:
|
||||
return
|
||||
_shop_npc_player_in_range = in_range
|
||||
if not in_range:
|
||||
_shop_npc_spoken_for_current_visit = false
|
||||
return
|
||||
_try_start_shop_npc_speech()
|
||||
|
||||
|
||||
func _try_start_shop_npc_speech() -> void:
|
||||
if (
|
||||
not _shop_npc_player_in_range
|
||||
or _shop_npc_spoken_for_current_visit
|
||||
or not _gameplay_ui_enabled
|
||||
):
|
||||
return
|
||||
_shop_npc_spoken_for_current_visit = true
|
||||
_ensure_shop_animalese_voice()
|
||||
TypewriterRevealType.start(
|
||||
_shop_prompt_message,
|
||||
|
|
@ -1384,8 +1414,6 @@ func set_shop_prompt_visible(
|
|||
SHOP_ANIMALESE_VOICE_ID,
|
||||
SHOP_SPEECH_CHARACTERS_PER_SECOND,
|
||||
)
|
||||
if world_anchor.is_finite():
|
||||
_position_shop_prompt(world_anchor)
|
||||
|
||||
|
||||
func _ensure_shop_animalese_voice() -> void:
|
||||
|
|
|
|||
92
ui/hotbar.gd
92
ui/hotbar.gd
|
|
@ -41,6 +41,12 @@ var _item_name_suppressed: bool = false
|
|||
var _motion_elapsed: float = 0.0
|
||||
var _compact_layout: bool = false
|
||||
var _player_menu_context: bool = false
|
||||
var _controller_placement_active: bool = false
|
||||
var _controller_placement_kind: PlayerHotbarType.AssignmentKind = (
|
||||
PlayerHotbarType.AssignmentKind.EMPTY
|
||||
)
|
||||
var _controller_placement_identity: StringName
|
||||
var _controller_placement_texture: Texture2D
|
||||
var _visibility_tween: Tween
|
||||
var _visibility_generation: int = 0
|
||||
|
||||
|
|
@ -99,6 +105,79 @@ func set_drag_enabled(enabled: bool) -> void:
|
|||
_hide_item_name()
|
||||
|
||||
|
||||
func begin_controller_placement(
|
||||
assignment_kind: PlayerHotbarType.AssignmentKind,
|
||||
identity: StringName,
|
||||
initial_slot: int,
|
||||
) -> void:
|
||||
if _hotbar == null or identity.is_empty():
|
||||
return
|
||||
_controller_placement_active = true
|
||||
_controller_placement_kind = assignment_kind
|
||||
_controller_placement_identity = identity
|
||||
_controller_placement_texture = _resolve_controller_placement_texture()
|
||||
var slot_count: int = _slots.size()
|
||||
for index: int in slot_count:
|
||||
var slot: BubbleHotbarSlotType = _slots[index]
|
||||
slot.focus_mode = Control.FOCUS_ALL
|
||||
slot.focus_neighbor_left = slot.get_path_to(
|
||||
_slots[wrapi(index - 1, 0, slot_count)]
|
||||
)
|
||||
slot.focus_neighbor_right = slot.get_path_to(
|
||||
_slots[wrapi(index + 1, 0, slot_count)]
|
||||
)
|
||||
slot.focus_neighbor_top = slot.get_path_to(slot)
|
||||
slot.focus_neighbor_bottom = slot.get_path_to(slot)
|
||||
var target_index: int = clampi(initial_slot, 0, slot_count - 1)
|
||||
_hotbar.select_slot(target_index)
|
||||
_refresh_controller_placement_preview()
|
||||
_slots[target_index].call_deferred("grab_focus")
|
||||
|
||||
|
||||
func end_controller_placement() -> void:
|
||||
if not _controller_placement_active:
|
||||
return
|
||||
_controller_placement_active = false
|
||||
_controller_placement_kind = PlayerHotbarType.AssignmentKind.EMPTY
|
||||
_controller_placement_identity = StringName()
|
||||
_controller_placement_texture = null
|
||||
for slot: BubbleHotbarSlotType in _slots:
|
||||
slot.focus_mode = Control.FOCUS_NONE
|
||||
slot.set_controller_placement_preview(false, null)
|
||||
_show_selected_item_briefly()
|
||||
|
||||
|
||||
func _resolve_controller_placement_texture() -> Texture2D:
|
||||
if (
|
||||
_controller_placement_kind == PlayerHotbarType.AssignmentKind.FISH
|
||||
and _fish_inventory != null
|
||||
):
|
||||
var fish_catch: FishCatchType = _fish_inventory.get_catch_by_id(
|
||||
_controller_placement_identity
|
||||
)
|
||||
return fish_catch.fish.display_texture if fish_catch != null else null
|
||||
if (
|
||||
_controller_placement_kind == PlayerHotbarType.AssignmentKind.ITEM
|
||||
and _catalog != null
|
||||
):
|
||||
var item: ItemDataType = _catalog.get_item_by_id(
|
||||
_controller_placement_identity
|
||||
)
|
||||
return item.icon if item != null else null
|
||||
return null
|
||||
|
||||
|
||||
func _refresh_controller_placement_preview() -> void:
|
||||
if not _controller_placement_active or _hotbar == null:
|
||||
return
|
||||
var target_index: int = _hotbar.get_selected_slot()
|
||||
for slot: BubbleHotbarSlotType in _slots:
|
||||
slot.set_controller_placement_preview(
|
||||
slot.slot_index == target_index,
|
||||
_controller_placement_texture,
|
||||
)
|
||||
|
||||
|
||||
func set_player_menu_context(enabled: bool) -> void:
|
||||
if _player_menu_context == enabled:
|
||||
return
|
||||
|
|
@ -230,6 +309,9 @@ func _collect_slots() -> void:
|
|||
slot.item_hover_ended.connect(_on_slot_item_hover_ended)
|
||||
slot.item_drag_started.connect(_on_slot_drag_started)
|
||||
slot.item_drag_finished.connect(_on_slot_drag_finished)
|
||||
slot.focus_entered.connect(
|
||||
_on_controller_slot_focused.bind(slot.slot_index)
|
||||
)
|
||||
_slots.append(slot)
|
||||
_slots.sort_custom(
|
||||
func(
|
||||
|
|
@ -278,10 +360,20 @@ func _on_selected_slot_changed(
|
|||
_item_id: StringName,
|
||||
) -> void:
|
||||
_refresh()
|
||||
if _controller_placement_active:
|
||||
_refresh_controller_placement_preview()
|
||||
return
|
||||
if _hovered_slot_index < 0:
|
||||
_show_selected_item_briefly()
|
||||
|
||||
|
||||
func _on_controller_slot_focused(slot_index: int) -> void:
|
||||
if not _controller_placement_active or _hotbar == null:
|
||||
return
|
||||
_hotbar.select_slot(slot_index)
|
||||
_refresh_controller_placement_preview()
|
||||
|
||||
|
||||
func _on_slot_item_hovered(
|
||||
slot_index: int,
|
||||
item_id: StringName,
|
||||
|
|
|
|||
|
|
@ -453,6 +453,7 @@ func _refresh_catalog() -> void:
|
|||
|
||||
func _make_entry(fish: FishDataType, discovered: bool) -> Button:
|
||||
var entry := Button.new()
|
||||
entry.set_meta(&"controller_focus_inversion_disabled", true)
|
||||
entry.custom_minimum_size = CATALOG_ENTRY_SIZE
|
||||
entry.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
entry.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
|
|
|
|||
456
ui/on_screen_keyboard.gd
Normal file
456
ui/on_screen_keyboard.gd
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
class_name OnScreenKeyboard
|
||||
extends Control
|
||||
|
||||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
const CHECK_ICON: Texture2D = preload(
|
||||
"res://ui/icons/pictograms/check_mark_dark.png"
|
||||
)
|
||||
const CHARACTER_KEY_SIZE: Vector2 = Vector2(96.0, 72.0)
|
||||
const CHARACTER_FONT_SIZE: int = 34
|
||||
const TRIGGER_PRESS_THRESHOLD: float = 0.55
|
||||
const TRIGGER_RELEASE_THRESHOLD: float = 0.25
|
||||
|
||||
signal text_submitted(value: String)
|
||||
|
||||
enum Page {
|
||||
LOWER,
|
||||
UPPER,
|
||||
SYMBOLS,
|
||||
}
|
||||
|
||||
var _enabled: bool = false
|
||||
var _page: Page = Page.LOWER
|
||||
var _target: Control
|
||||
var _target_virtual_keyboard_enabled: bool = true
|
||||
var _buffer: String = ""
|
||||
var _preview: Label
|
||||
var _page_buttons: Array[Button] = []
|
||||
var _keys_host: VBoxContainer
|
||||
var _key_buttons: Array[Button] = []
|
||||
var _caret_left_button: Button
|
||||
var _caret_right_button: Button
|
||||
var _backspace_button: Button
|
||||
var _space_button: Button
|
||||
var _check_button: Button
|
||||
var _left_trigger_pressed: bool = false
|
||||
var _right_trigger_pressed: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
z_index = 1000
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_build_interface()
|
||||
hide()
|
||||
set_process_input(true)
|
||||
|
||||
|
||||
func set_enabled(enabled: bool) -> void:
|
||||
_enabled = enabled
|
||||
if not _enabled and visible:
|
||||
_close_keyboard(false)
|
||||
|
||||
|
||||
func is_enabled() -> bool:
|
||||
return _enabled
|
||||
|
||||
|
||||
func is_open() -> bool:
|
||||
return visible
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not _enabled:
|
||||
return
|
||||
if visible:
|
||||
var joy_motion := event as InputEventJoypadMotion
|
||||
if joy_motion != null and _handle_trigger_shortcut(joy_motion):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var joy_button := event as InputEventJoypadButton
|
||||
if joy_button != null and joy_button.pressed:
|
||||
if joy_button.button_index == JOY_BUTTON_X:
|
||||
_backspace()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if joy_button.button_index == JOY_BUTTON_LEFT_SHOULDER:
|
||||
_close_keyboard(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if joy_button.button_index == JOY_BUTTON_RIGHT_SHOULDER:
|
||||
_set_page(wrapi(int(_page) + 1, 0, Page.size()))
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event.is_action_pressed(&"ui_cancel"):
|
||||
_close_keyboard(true)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var joy_event := event as InputEventJoypadButton
|
||||
if (
|
||||
joy_event == null
|
||||
or not joy_event.pressed
|
||||
or (
|
||||
joy_event.button_index != JOY_BUTTON_A
|
||||
and not event.is_action_pressed(&"ui_accept")
|
||||
)
|
||||
):
|
||||
return
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if _can_edit(focus_owner):
|
||||
_open_for(focus_owner)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _can_edit(control: Control) -> bool:
|
||||
if control is LineEdit:
|
||||
return (control as LineEdit).editable
|
||||
if control is TextEdit:
|
||||
return (control as TextEdit).editable
|
||||
return false
|
||||
|
||||
|
||||
func _open_for(control: Control) -> void:
|
||||
_target = control
|
||||
_target_virtual_keyboard_enabled = bool(
|
||||
_target.get("virtual_keyboard_enabled")
|
||||
)
|
||||
_target.set("virtual_keyboard_enabled", false)
|
||||
_buffer = str(_target.get("text"))
|
||||
_page = Page.LOWER
|
||||
show()
|
||||
_refresh_preview()
|
||||
_rebuild_keys()
|
||||
|
||||
|
||||
func _close_keyboard(restore_focus: bool) -> void:
|
||||
var prior_target: Control = _target
|
||||
if is_instance_valid(prior_target):
|
||||
prior_target.set(
|
||||
"virtual_keyboard_enabled",
|
||||
_target_virtual_keyboard_enabled
|
||||
)
|
||||
hide()
|
||||
get_viewport().gui_release_focus()
|
||||
_target = null
|
||||
if restore_focus and is_instance_valid(prior_target):
|
||||
prior_target.grab_focus()
|
||||
|
||||
|
||||
func _submit() -> void:
|
||||
var submitted_target: Control = _target
|
||||
var submitted_text: String = _buffer
|
||||
_close_keyboard(false)
|
||||
if submitted_target is LineEdit:
|
||||
(submitted_target as LineEdit).text_submitted.emit(submitted_text)
|
||||
elif submitted_target is TextEdit:
|
||||
(submitted_target as TextEdit).text_changed.emit()
|
||||
text_submitted.emit(submitted_text)
|
||||
|
||||
|
||||
func _type_character(character: String) -> void:
|
||||
if not is_instance_valid(_target):
|
||||
_close_keyboard(false)
|
||||
return
|
||||
var caret: int = _get_caret_column()
|
||||
var next_text: String = (
|
||||
_buffer.substr(0, caret)
|
||||
+ character
|
||||
+ _buffer.substr(caret)
|
||||
)
|
||||
if _target is LineEdit:
|
||||
var line_edit := _target as LineEdit
|
||||
if line_edit.max_length > 0 and next_text.length() > line_edit.max_length:
|
||||
return
|
||||
_buffer = next_text
|
||||
_set_target_text(caret + character.length())
|
||||
|
||||
|
||||
func _type_space() -> void:
|
||||
_type_character(" ")
|
||||
|
||||
|
||||
func _backspace() -> void:
|
||||
if not is_instance_valid(_target) or _buffer.is_empty():
|
||||
return
|
||||
var caret: int = _get_caret_column()
|
||||
if caret <= 0:
|
||||
return
|
||||
_buffer = _buffer.erase(caret - 1, 1)
|
||||
_set_target_text(caret - 1)
|
||||
|
||||
|
||||
func _move_caret(direction: int) -> void:
|
||||
if not is_instance_valid(_target):
|
||||
_close_keyboard(false)
|
||||
return
|
||||
var caret: int = clampi(
|
||||
_get_caret_column() + direction,
|
||||
0,
|
||||
_buffer.length(),
|
||||
)
|
||||
if _target is LineEdit:
|
||||
(_target as LineEdit).caret_column = caret
|
||||
elif _target is TextEdit:
|
||||
(_target as TextEdit).set_caret_column(caret)
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
func _handle_trigger_shortcut(event: InputEventJoypadMotion) -> bool:
|
||||
if event.axis == JOY_AXIS_TRIGGER_LEFT:
|
||||
if event.axis_value <= TRIGGER_RELEASE_THRESHOLD:
|
||||
_left_trigger_pressed = false
|
||||
elif (
|
||||
event.axis_value >= TRIGGER_PRESS_THRESHOLD
|
||||
and not _left_trigger_pressed
|
||||
):
|
||||
_left_trigger_pressed = true
|
||||
_move_caret(-1)
|
||||
return true
|
||||
elif event.axis == JOY_AXIS_TRIGGER_RIGHT:
|
||||
if event.axis_value <= TRIGGER_RELEASE_THRESHOLD:
|
||||
_right_trigger_pressed = false
|
||||
elif (
|
||||
event.axis_value >= TRIGGER_PRESS_THRESHOLD
|
||||
and not _right_trigger_pressed
|
||||
):
|
||||
_right_trigger_pressed = true
|
||||
_move_caret(1)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _get_caret_column() -> int:
|
||||
if _target is LineEdit:
|
||||
return clampi(
|
||||
(_target as LineEdit).caret_column,
|
||||
0,
|
||||
_buffer.length()
|
||||
)
|
||||
return _buffer.length()
|
||||
|
||||
|
||||
func _set_target_text(caret: int) -> void:
|
||||
if _target is LineEdit:
|
||||
var line_edit := _target as LineEdit
|
||||
line_edit.text = _buffer
|
||||
line_edit.caret_column = caret
|
||||
line_edit.text_changed.emit(_buffer)
|
||||
elif _target is TextEdit:
|
||||
var text_edit := _target as TextEdit
|
||||
text_edit.text = _buffer
|
||||
text_edit.text_changed.emit()
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
func _refresh_preview() -> void:
|
||||
if _preview == null:
|
||||
return
|
||||
var displayed_text: String = _buffer
|
||||
if _target is LineEdit and (_target as LineEdit).secret:
|
||||
displayed_text = "*".repeat(_buffer.length())
|
||||
var caret: int = clampi(_get_caret_column(), 0, displayed_text.length())
|
||||
_preview.text = displayed_text.insert(caret, "|")
|
||||
|
||||
|
||||
func _set_page(page_index: int) -> void:
|
||||
_page = page_index as Page
|
||||
_rebuild_keys()
|
||||
|
||||
|
||||
func _rebuild_keys() -> void:
|
||||
for child: Node in _keys_host.get_children():
|
||||
_keys_host.remove_child(child)
|
||||
child.queue_free()
|
||||
_key_buttons.clear()
|
||||
var rows: Array = _rows_for_page()
|
||||
for row_value: Variant in rows:
|
||||
var row := HBoxContainer.new()
|
||||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
row.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
row.add_theme_constant_override("separation", 10)
|
||||
_keys_host.add_child(row)
|
||||
for character_value: Variant in row_value as Array:
|
||||
var character: String = str(character_value)
|
||||
var key_button: Button = _make_button(character, CHARACTER_KEY_SIZE)
|
||||
key_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
key_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
key_button.add_theme_font_size_override(
|
||||
"font_size", CHARACTER_FONT_SIZE
|
||||
)
|
||||
key_button.pressed.connect(_type_character.bind(character))
|
||||
row.add_child(key_button)
|
||||
_key_buttons.append(key_button)
|
||||
for index: int in _page_buttons.size():
|
||||
_page_buttons[index].button_pressed = index == int(_page)
|
||||
call_deferred("_configure_key_focus")
|
||||
|
||||
|
||||
func _configure_key_focus() -> void:
|
||||
var focus_controls: Array[Control] = []
|
||||
for button: Button in _key_buttons:
|
||||
focus_controls.append(button)
|
||||
for button: Button in [
|
||||
_caret_left_button,
|
||||
_caret_right_button,
|
||||
_backspace_button,
|
||||
_space_button,
|
||||
_check_button,
|
||||
]:
|
||||
focus_controls.append(button)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(focus_controls)
|
||||
if not _key_buttons.is_empty():
|
||||
_key_buttons[0].grab_focus()
|
||||
|
||||
|
||||
func _rows_for_page() -> Array:
|
||||
match _page:
|
||||
Page.UPPER:
|
||||
return [
|
||||
["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
|
||||
["A", "S", "D", "F", "G", "H", "J", "K", "L"],
|
||||
["Z", "X", "C", "V", "B", "N", "M"],
|
||||
]
|
||||
Page.SYMBOLS:
|
||||
return [
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
||||
["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"],
|
||||
["-", "_", "=", "+", "[", "]", "{", "}"],
|
||||
[".", ",", "?", "/", ":", ";", "'", "\""]
|
||||
]
|
||||
_:
|
||||
return [
|
||||
["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
|
||||
["a", "s", "d", "f", "g", "h", "j", "k", "l"],
|
||||
["z", "x", "c", "v", "b", "n", "m"],
|
||||
]
|
||||
|
||||
|
||||
func _build_interface() -> void:
|
||||
var backdrop := ColorRect.new()
|
||||
backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
backdrop.color = Color(0.012, 0.075, 0.105, 0.82)
|
||||
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
add_child(backdrop)
|
||||
var screen_margin := MarginContainer.new()
|
||||
screen_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
for side: StringName in [
|
||||
&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"
|
||||
]:
|
||||
screen_margin.add_theme_constant_override(side, 14)
|
||||
add_child(screen_margin)
|
||||
var panel := PanelContainer.new()
|
||||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
panel.add_theme_stylebox_override(
|
||||
"panel",
|
||||
_make_style(Color(0.075, 0.27, 0.34, 0.98), 18, 4)
|
||||
)
|
||||
screen_margin.add_child(panel)
|
||||
var margin := MarginContainer.new()
|
||||
for side: StringName in [&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"]:
|
||||
margin.add_theme_constant_override(side, 18)
|
||||
panel.add_child(margin)
|
||||
var layout := VBoxContainer.new()
|
||||
layout.add_theme_constant_override("separation", 10)
|
||||
margin.add_child(layout)
|
||||
var preview_panel := PanelContainer.new()
|
||||
preview_panel.custom_minimum_size = Vector2(0.0, 86.0)
|
||||
preview_panel.add_theme_stylebox_override(
|
||||
"panel",
|
||||
_make_style(Color(0.82, 0.94, 0.95, 1.0), 10, 2)
|
||||
)
|
||||
layout.add_child(preview_panel)
|
||||
_preview = Label.new()
|
||||
_preview.add_theme_color_override("font_color", Color(0.025, 0.12, 0.17, 1.0))
|
||||
_preview.add_theme_font_size_override("font_size", 30)
|
||||
_preview.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
_preview.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_preview.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
_preview.add_theme_constant_override("outline_size", 0)
|
||||
preview_panel.add_child(_preview)
|
||||
var page_row := HBoxContainer.new()
|
||||
page_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
page_row.add_theme_constant_override("separation", 12)
|
||||
layout.add_child(page_row)
|
||||
for page_name: String in ["lower", "upper", "symbols"]:
|
||||
var page_button: Button = _make_button(page_name, Vector2(150.0, 44.0))
|
||||
page_button.toggle_mode = true
|
||||
page_button.focus_mode = Control.FOCUS_NONE
|
||||
page_button.pressed.connect(_set_page.bind(_page_buttons.size()))
|
||||
page_row.add_child(page_button)
|
||||
_page_buttons.append(page_button)
|
||||
_keys_host = VBoxContainer.new()
|
||||
_keys_host.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_keys_host.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
_keys_host.add_theme_constant_override("separation", 12)
|
||||
layout.add_child(_keys_host)
|
||||
var utility_row := HBoxContainer.new()
|
||||
utility_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
utility_row.add_theme_constant_override("separation", 12)
|
||||
layout.add_child(utility_row)
|
||||
_caret_left_button = _make_button("LT <", Vector2(118.0, 64.0))
|
||||
_caret_left_button.tooltip_text = "move text cursor left"
|
||||
_caret_left_button.pressed.connect(_move_caret.bind(-1))
|
||||
utility_row.add_child(_caret_left_button)
|
||||
_caret_right_button = _make_button("> RT", Vector2(118.0, 64.0))
|
||||
_caret_right_button.tooltip_text = "move text cursor right"
|
||||
_caret_right_button.pressed.connect(_move_caret.bind(1))
|
||||
utility_row.add_child(_caret_right_button)
|
||||
_backspace_button = _make_button("backspace X", Vector2(180.0, 64.0))
|
||||
_backspace_button.pressed.connect(_backspace)
|
||||
utility_row.add_child(_backspace_button)
|
||||
_space_button = _make_button("space", Vector2(350.0, 64.0))
|
||||
_space_button.size_flags_stretch_ratio = 4.0
|
||||
_space_button.pressed.connect(_type_space)
|
||||
utility_row.add_child(_space_button)
|
||||
_check_button = _make_button("", Vector2(92.0, 64.0))
|
||||
_check_button.icon = CHECK_ICON
|
||||
_check_button.expand_icon = true
|
||||
_check_button.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_check_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_check_button.vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_check_button.tooltip_text = "submit"
|
||||
_check_button.accessibility_name = "submit text"
|
||||
_check_button.add_theme_constant_override("icon_max_width", 42)
|
||||
_check_button.pressed.connect(_submit)
|
||||
utility_row.add_child(_check_button)
|
||||
_rebuild_keys()
|
||||
|
||||
|
||||
func _make_button(label: String, minimum_size: Vector2) -> Button:
|
||||
var button := Button.new()
|
||||
button.text = label
|
||||
button.custom_minimum_size = minimum_size
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
button.add_theme_font_size_override("font_size", 22)
|
||||
button.add_theme_color_override("font_color", Color(0.025, 0.12, 0.17, 1.0))
|
||||
button.add_theme_color_override("font_hover_color", Color(0.025, 0.12, 0.17, 1.0))
|
||||
button.add_theme_color_override("font_focus_color", Color(0.025, 0.12, 0.17, 1.0))
|
||||
button.add_theme_stylebox_override(
|
||||
"normal",
|
||||
_make_style(Color(0.72, 0.88, 0.91, 1.0), 10, 2)
|
||||
)
|
||||
button.add_theme_stylebox_override(
|
||||
"hover",
|
||||
_make_style(Color(0.87, 0.96, 0.96, 1.0), 10, 3)
|
||||
)
|
||||
button.add_theme_stylebox_override("focus", button.get_theme_stylebox("hover"))
|
||||
button.add_theme_stylebox_override(
|
||||
"pressed",
|
||||
_make_style(Color(0.44, 0.72, 0.77, 1.0), 10, 3)
|
||||
)
|
||||
return button
|
||||
|
||||
|
||||
func _make_style(color: Color, radius: int, border_width: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
style.border_color = Color(0.025, 0.12, 0.17, 1.0)
|
||||
style.set_border_width_all(border_width)
|
||||
style.set_corner_radius_all(radius)
|
||||
return style
|
||||
1
ui/on_screen_keyboard.gd.uid
Normal file
1
ui/on_screen_keyboard.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dcanibf8hordh
|
||||
|
|
@ -85,6 +85,12 @@ const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop"
|
|||
|
||||
signal menu_visibility_changed(is_open: bool)
|
||||
signal inventory_hotbar_context_changed(show_hotbar: bool)
|
||||
signal controller_hotbar_placement_requested(
|
||||
assignment_kind: PlayerHotbarType.AssignmentKind,
|
||||
identity: StringName,
|
||||
initial_slot: int,
|
||||
)
|
||||
signal controller_hotbar_placement_ended
|
||||
signal menu_exit_started
|
||||
signal shop_cooler_modal_changed(is_open: bool)
|
||||
|
||||
|
|
@ -124,6 +130,12 @@ enum CloseReason {
|
|||
TEARDOWN,
|
||||
}
|
||||
|
||||
enum ControllerOwnership {
|
||||
ITEM_LIST,
|
||||
NOTEPAD_ACTIONS,
|
||||
HOTBAR_PLACEMENT,
|
||||
}
|
||||
|
||||
const INVENTORY_MAIN_POSITION := Vector2(54.0, 166.0)
|
||||
const INVENTORY_MAIN_SIZE := Vector2(882.0, 484.0)
|
||||
const INVENTORY_PANEL_GAP := 16.0
|
||||
|
|
@ -302,6 +314,15 @@ var _last_inventory_section: Section = Section.COOLER
|
|||
var _bag_view: BagView = BagView.EQUIPMENT
|
||||
var _tackle_view: TackleView = TackleView.BAIT
|
||||
var _selected_tackle_item_id: StringName
|
||||
var _controller_ownership: ControllerOwnership = ControllerOwnership.ITEM_LIST
|
||||
var _controller_source_section: Section = Section.COOLER
|
||||
var _controller_source_identity: StringName
|
||||
var _controller_notepad_actions: Array[BaseButton] = []
|
||||
var _controller_hotbar_assignment_kind: PlayerHotbarType.AssignmentKind = (
|
||||
PlayerHotbarType.AssignmentKind.EMPTY
|
||||
)
|
||||
var _controller_hotbar_identity: StringName
|
||||
var _controller_previous_hotbar_slot: int = 0
|
||||
var _sort_mode: SortMode = SortMode.CATCH_ORDER
|
||||
var _sort_descending: bool = true
|
||||
var _fish_selection := FishBatchSelectionType.new()
|
||||
|
|
@ -589,12 +610,25 @@ func set_profile_preview_world_pixel_size(pixel_size: int) -> void:
|
|||
_profile_page.set_world_pixel_size(pixel_size)
|
||||
|
||||
|
||||
var _left_page_trigger_held: bool = false
|
||||
var _right_page_trigger_held: bool = false
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.echo:
|
||||
return
|
||||
if _handle_controller_ownership_input(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if visible:
|
||||
_reserve_main_navigation_for_page_switching()
|
||||
_reserve_visible_secondary_navigation()
|
||||
if _handle_controller_page_switch(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _handle_controller_secondary_switch(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _handle_direct_page_shortcut(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
|
@ -613,47 +647,360 @@ func _input(event: InputEvent) -> void:
|
|||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
||||
if not visible:
|
||||
return false
|
||||
var button_event := event as InputEventJoypadButton
|
||||
var accept_pressed: bool = (
|
||||
button_event != null
|
||||
and button_event.pressed
|
||||
and _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_A,
|
||||
JOY_BUTTON_A,
|
||||
)
|
||||
)
|
||||
var cancel_pressed: bool = (
|
||||
button_event != null
|
||||
and button_event.pressed
|
||||
and _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_B,
|
||||
JOY_BUTTON_B,
|
||||
)
|
||||
)
|
||||
var alternate_pressed: bool = (
|
||||
button_event != null
|
||||
and button_event.pressed
|
||||
and _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_Y,
|
||||
JOY_BUTTON_Y,
|
||||
)
|
||||
)
|
||||
if _controller_ownership == ControllerOwnership.HOTBAR_PLACEMENT:
|
||||
if accept_pressed:
|
||||
_confirm_controller_hotbar_placement()
|
||||
return true
|
||||
if cancel_pressed:
|
||||
_release_controller_ownership(true, true)
|
||||
return true
|
||||
if alternate_pressed:
|
||||
return true
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.NOTEPAD_ACTIONS:
|
||||
if cancel_pressed:
|
||||
_release_controller_ownership(true, false)
|
||||
return true
|
||||
if alternate_pressed:
|
||||
return true
|
||||
var direction: int = 0
|
||||
if event.is_action_pressed("ui_left") or event.is_action_pressed("ui_up"):
|
||||
direction = -1
|
||||
elif (
|
||||
event.is_action_pressed("ui_right")
|
||||
or event.is_action_pressed("ui_down")
|
||||
):
|
||||
direction = 1
|
||||
if direction != 0:
|
||||
_focus_next_notepad_action(direction)
|
||||
return true
|
||||
return false
|
||||
if alternate_pressed:
|
||||
return _try_begin_controller_hotbar_placement()
|
||||
if accept_pressed:
|
||||
return _try_enter_notepad_controller_ownership()
|
||||
return false
|
||||
|
||||
|
||||
func _event_matches_controller_role(
|
||||
event: InputEvent,
|
||||
role: StringName,
|
||||
fallback_button: JoyButton,
|
||||
) -> bool:
|
||||
if _controller_mapping_manager != null:
|
||||
return _controller_mapping_manager.event_matches_role(event, role)
|
||||
var button_event := event as InputEventJoypadButton
|
||||
return button_event != null and button_event.button_index == fallback_button
|
||||
|
||||
|
||||
func _try_enter_notepad_controller_ownership() -> bool:
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if focus_owner == null:
|
||||
return false
|
||||
var source_identity: StringName
|
||||
var actions: Array[BaseButton] = []
|
||||
if _current_section == Section.COOLER:
|
||||
var fish_node := focus_owner as CoolerFishSpriteType
|
||||
if fish_node == null or fish_node.catch_id.is_empty():
|
||||
return false
|
||||
source_identity = fish_node.catch_id
|
||||
_on_catch_card_pressed(source_identity)
|
||||
if not _favorite_bubble.disabled:
|
||||
actions.append(_favorite_bubble)
|
||||
if not _sell_bubble.disabled:
|
||||
actions.append(_sell_bubble)
|
||||
elif _current_section == Section.TACKLE_BOX:
|
||||
if not focus_owner.has_meta(&"controller_tackle_item_id"):
|
||||
return false
|
||||
source_identity = StringName(
|
||||
str(focus_owner.get_meta(&"controller_tackle_item_id"))
|
||||
)
|
||||
_select_tackle_item(source_identity)
|
||||
if _tackle_equip_button.visible and not _tackle_equip_button.disabled:
|
||||
actions.append(_tackle_equip_button)
|
||||
else:
|
||||
return false
|
||||
if actions.is_empty():
|
||||
_restore_controller_item_focus(_current_section, source_identity)
|
||||
return true
|
||||
_controller_source_section = _current_section
|
||||
_controller_source_identity = source_identity
|
||||
_controller_notepad_actions = actions
|
||||
_controller_ownership = ControllerOwnership.NOTEPAD_ACTIONS
|
||||
actions.front().call_deferred("grab_focus")
|
||||
return true
|
||||
|
||||
|
||||
func _focus_next_notepad_action(direction: int) -> void:
|
||||
var available: Array[BaseButton] = []
|
||||
for action: BaseButton in _controller_notepad_actions:
|
||||
if (
|
||||
is_instance_valid(action)
|
||||
and action.visible
|
||||
and not action.disabled
|
||||
and action.focus_mode != Control.FOCUS_NONE
|
||||
):
|
||||
available.append(action)
|
||||
if available.is_empty():
|
||||
return
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
var current_index: int = available.find(focused)
|
||||
if current_index < 0:
|
||||
current_index = 0 if direction > 0 else available.size() - 1
|
||||
else:
|
||||
current_index = wrapi(
|
||||
current_index + direction,
|
||||
0,
|
||||
available.size(),
|
||||
)
|
||||
available[current_index].grab_focus()
|
||||
|
||||
|
||||
func _try_begin_controller_hotbar_placement() -> bool:
|
||||
if _hotbar == null:
|
||||
return false
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if focus_owner == null:
|
||||
return false
|
||||
var assignment_kind: PlayerHotbarType.AssignmentKind = (
|
||||
PlayerHotbarType.AssignmentKind.EMPTY
|
||||
)
|
||||
var identity: StringName
|
||||
if _current_section == Section.COOLER:
|
||||
var fish_node := focus_owner as CoolerFishSpriteType
|
||||
if fish_node == null or fish_node.catch_id.is_empty():
|
||||
return false
|
||||
assignment_kind = PlayerHotbarType.AssignmentKind.FISH
|
||||
identity = fish_node.catch_id
|
||||
elif _current_section == Section.BAG:
|
||||
var item_node := focus_owner as BagItemSpriteType
|
||||
if item_node == null or item_node.item_id.is_empty():
|
||||
return false
|
||||
var item: ItemDataType = (
|
||||
_item_catalog.get_item_by_id(item_node.item_id)
|
||||
if _item_catalog != null
|
||||
else null
|
||||
)
|
||||
if item == null or not item.hotbar_allowed:
|
||||
return true
|
||||
assignment_kind = PlayerHotbarType.AssignmentKind.ITEM
|
||||
identity = item_node.item_id
|
||||
else:
|
||||
return false
|
||||
_controller_source_section = _current_section
|
||||
_controller_source_identity = identity
|
||||
_controller_hotbar_assignment_kind = assignment_kind
|
||||
_controller_hotbar_identity = identity
|
||||
_controller_previous_hotbar_slot = _hotbar.get_selected_slot()
|
||||
var initial_slot: int = _find_controller_hotbar_assignment(
|
||||
assignment_kind,
|
||||
identity,
|
||||
)
|
||||
if initial_slot < 0:
|
||||
initial_slot = _controller_previous_hotbar_slot
|
||||
_controller_ownership = ControllerOwnership.HOTBAR_PLACEMENT
|
||||
controller_hotbar_placement_requested.emit(
|
||||
assignment_kind,
|
||||
identity,
|
||||
initial_slot,
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
func _find_controller_hotbar_assignment(
|
||||
assignment_kind: PlayerHotbarType.AssignmentKind,
|
||||
identity: StringName,
|
||||
) -> int:
|
||||
for slot_index: int in range(PlayerHotbarType.SLOT_COUNT):
|
||||
if (
|
||||
assignment_kind == PlayerHotbarType.AssignmentKind.FISH
|
||||
and _hotbar.get_fish_catch_id(slot_index) == identity
|
||||
):
|
||||
return slot_index
|
||||
if (
|
||||
assignment_kind == PlayerHotbarType.AssignmentKind.ITEM
|
||||
and _hotbar.get_item_id(slot_index) == identity
|
||||
):
|
||||
return slot_index
|
||||
return -1
|
||||
|
||||
|
||||
func _confirm_controller_hotbar_placement() -> void:
|
||||
if (
|
||||
_controller_ownership != ControllerOwnership.HOTBAR_PLACEMENT
|
||||
or _hotbar == null
|
||||
):
|
||||
return
|
||||
var slot_index: int = _hotbar.get_selected_slot()
|
||||
var assigned: bool = false
|
||||
if (
|
||||
_controller_hotbar_assignment_kind
|
||||
== PlayerHotbarType.AssignmentKind.FISH
|
||||
):
|
||||
assigned = _hotbar.assign_fish(
|
||||
slot_index,
|
||||
_controller_hotbar_identity,
|
||||
)
|
||||
elif (
|
||||
_controller_hotbar_assignment_kind
|
||||
== PlayerHotbarType.AssignmentKind.ITEM
|
||||
):
|
||||
assigned = _hotbar.assign_item(
|
||||
slot_index,
|
||||
_controller_hotbar_identity,
|
||||
)
|
||||
if assigned:
|
||||
_release_controller_ownership(true, false)
|
||||
|
||||
|
||||
func _release_controller_ownership(
|
||||
restore_source_focus: bool,
|
||||
restore_previous_hotbar_slot: bool,
|
||||
) -> void:
|
||||
if _controller_ownership == ControllerOwnership.ITEM_LIST:
|
||||
return
|
||||
var prior_ownership: ControllerOwnership = _controller_ownership
|
||||
var source_section: Section = _controller_source_section
|
||||
var source_identity: StringName = _controller_source_identity
|
||||
_controller_ownership = ControllerOwnership.ITEM_LIST
|
||||
_controller_notepad_actions.clear()
|
||||
_controller_source_identity = StringName()
|
||||
_controller_hotbar_assignment_kind = PlayerHotbarType.AssignmentKind.EMPTY
|
||||
_controller_hotbar_identity = StringName()
|
||||
if prior_ownership == ControllerOwnership.HOTBAR_PLACEMENT:
|
||||
if restore_previous_hotbar_slot and _hotbar != null:
|
||||
_hotbar.select_slot(_controller_previous_hotbar_slot)
|
||||
controller_hotbar_placement_ended.emit()
|
||||
if restore_source_focus:
|
||||
call_deferred(
|
||||
"_restore_controller_item_focus",
|
||||
source_section,
|
||||
source_identity,
|
||||
)
|
||||
|
||||
|
||||
func _restore_controller_item_focus(
|
||||
section: Section,
|
||||
identity: StringName,
|
||||
) -> void:
|
||||
if not visible or section != _current_section or identity.is_empty():
|
||||
return
|
||||
var target: Control
|
||||
if section == Section.COOLER:
|
||||
target = _fish_nodes.get(identity) as CoolerFishSpriteType
|
||||
elif section == Section.BAG:
|
||||
target = _bag_item_nodes.get(identity) as BagItemSpriteType
|
||||
elif section == Section.TACKLE_BOX:
|
||||
for child: Node in _tackle_item_list.get_children():
|
||||
var button := child as BaseButton
|
||||
if (
|
||||
button != null
|
||||
and button.has_meta(&"controller_tackle_item_id")
|
||||
and StringName(str(button.get_meta(
|
||||
&"controller_tackle_item_id"
|
||||
))) == identity
|
||||
):
|
||||
target = button
|
||||
break
|
||||
if (
|
||||
target != null
|
||||
and is_instance_valid(target)
|
||||
and target.is_visible_in_tree()
|
||||
and target.focus_mode != Control.FOCUS_NONE
|
||||
):
|
||||
target.grab_focus()
|
||||
|
||||
|
||||
func _handle_controller_page_switch(event: InputEvent) -> bool:
|
||||
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
|
||||
var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
|
||||
var use_mapping: bool = (
|
||||
_controller_mapping_manager != null
|
||||
)
|
||||
var uses_left_bumper: bool = (
|
||||
var uses_left_trigger: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event, ControllerMappingManagerType.ROLE_LB
|
||||
event, ControllerMappingManagerType.ROLE_POINTER_MODIFIER
|
||||
)
|
||||
if use_mapping
|
||||
else (
|
||||
button_event != null
|
||||
and button_event.button_index == JOY_BUTTON_LEFT_SHOULDER
|
||||
motion_event != null
|
||||
and motion_event.axis == JOY_AXIS_TRIGGER_LEFT
|
||||
)
|
||||
)
|
||||
var uses_right_bumper: bool = (
|
||||
var uses_right_trigger: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event, ControllerMappingManagerType.ROLE_RB
|
||||
event, ControllerMappingManagerType.ROLE_CAMERA_ZOOM
|
||||
)
|
||||
if use_mapping
|
||||
else (
|
||||
button_event != null
|
||||
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
|
||||
motion_event != null
|
||||
and motion_event.axis == JOY_AXIS_TRIGGER_RIGHT
|
||||
)
|
||||
)
|
||||
if (
|
||||
button_event == null
|
||||
or not (uses_left_bumper or uses_right_bumper)
|
||||
not (uses_left_trigger or uses_right_trigger)
|
||||
or not visible
|
||||
):
|
||||
return false
|
||||
if not button_event.pressed:
|
||||
var is_pressed: bool = (
|
||||
button_event.pressed
|
||||
if button_event != null
|
||||
else motion_event.axis_value > 0.5
|
||||
)
|
||||
if not is_pressed:
|
||||
if uses_left_trigger:
|
||||
_left_page_trigger_held = false
|
||||
if uses_right_trigger:
|
||||
_right_page_trigger_held = false
|
||||
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 (
|
||||
(uses_left_trigger and _left_page_trigger_held)
|
||||
or (uses_right_trigger and _right_page_trigger_held)
|
||||
):
|
||||
return true
|
||||
if uses_left_trigger:
|
||||
_left_page_trigger_held = true
|
||||
if uses_right_trigger:
|
||||
_right_page_trigger_held = true
|
||||
# Trigger input belongs to the Player Menu while it is visible, even when a
|
||||
# transition or modal temporarily prevents changing pages.
|
||||
if (
|
||||
_transitioning
|
||||
or _page_transitioning
|
||||
or _sale_confirmation.visible
|
||||
or get_viewport().gui_is_dragging()
|
||||
or _controller_ownership != ControllerOwnership.ITEM_LIST
|
||||
):
|
||||
return true
|
||||
var sections: Array[Section] = [
|
||||
|
|
@ -670,13 +1017,131 @@ func _handle_controller_page_switch(event: InputEvent) -> bool:
|
|||
if current_index < 0:
|
||||
current_index = 0
|
||||
var direction: int = (
|
||||
-1 if uses_left_bumper else 1
|
||||
-1 if uses_left_trigger else 1
|
||||
)
|
||||
var next_index: int = wrapi(current_index + direction, 0, sections.size())
|
||||
_show_section(sections[next_index])
|
||||
return true
|
||||
|
||||
|
||||
func _handle_controller_secondary_switch(event: InputEvent) -> bool:
|
||||
var button_event := event as InputEventJoypadButton
|
||||
if button_event == null or not visible:
|
||||
return false
|
||||
var use_mapping: bool = _controller_mapping_manager != null
|
||||
var uses_left_bumper: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event, ControllerMappingManagerType.ROLE_LB
|
||||
)
|
||||
if use_mapping
|
||||
else 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.button_index == JOY_BUTTON_RIGHT_SHOULDER
|
||||
)
|
||||
if not (uses_left_bumper or uses_right_bumper):
|
||||
return false
|
||||
if not button_event.pressed:
|
||||
return true
|
||||
if (
|
||||
_transitioning
|
||||
or _page_transitioning
|
||||
or _sale_confirmation.visible
|
||||
or get_viewport().gui_is_dragging()
|
||||
or _controller_ownership != ControllerOwnership.ITEM_LIST
|
||||
):
|
||||
return true
|
||||
var direction: int = -1 if uses_left_bumper else 1
|
||||
if _is_inventory_section(_current_section):
|
||||
var inventory_sections: Array[Section] = [
|
||||
Section.COOLER,
|
||||
Section.BAG,
|
||||
Section.TACKLE_BOX,
|
||||
]
|
||||
var inventory_index: int = inventory_sections.find(_current_section)
|
||||
_show_section(inventory_sections[wrapi(
|
||||
inventory_index + direction,
|
||||
0,
|
||||
inventory_sections.size(),
|
||||
)])
|
||||
return true
|
||||
_cycle_visible_secondary_tabs(direction)
|
||||
return true
|
||||
|
||||
|
||||
func _cycle_visible_secondary_tabs(direction: int) -> bool:
|
||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
||||
var grouped_buttons: Dictionary = {}
|
||||
_collect_visible_toggle_buttons(self, navigation_cluster, grouped_buttons)
|
||||
var selected_group: Variant = null
|
||||
var selected_group_y: float = INF
|
||||
for group_value: Variant in grouped_buttons.keys():
|
||||
var buttons := grouped_buttons[group_value] as Array
|
||||
if buttons.size() < 2:
|
||||
continue
|
||||
var group_y: float = INF
|
||||
for item: Variant in buttons:
|
||||
var button := item as BaseButton
|
||||
group_y = minf(group_y, button.global_position.y)
|
||||
if group_y < selected_group_y:
|
||||
selected_group = group_value
|
||||
selected_group_y = group_y
|
||||
if selected_group == null:
|
||||
return false
|
||||
var tabs := grouped_buttons[selected_group] as Array
|
||||
tabs.sort_custom(func(first: BaseButton, second: BaseButton) -> bool:
|
||||
return first.global_position.x < second.global_position.x
|
||||
)
|
||||
var current_index: int = 0
|
||||
for index: int in tabs.size():
|
||||
var tab := tabs[index] as BaseButton
|
||||
if tab.button_pressed:
|
||||
current_index = index
|
||||
break
|
||||
var target := tabs[wrapi(
|
||||
current_index + direction, 0, tabs.size()
|
||||
)] as BaseButton
|
||||
target.set_pressed_no_signal(true)
|
||||
target.pressed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func _collect_visible_toggle_buttons(
|
||||
root: Node,
|
||||
navigation_cluster: Control,
|
||||
grouped_buttons: Dictionary,
|
||||
) -> void:
|
||||
for child: Node in root.get_children():
|
||||
var child_control := child as Control
|
||||
if child_control != null and not child_control.is_visible_in_tree():
|
||||
continue
|
||||
var button := child as BaseButton
|
||||
if (
|
||||
button != null
|
||||
and button.toggle_mode
|
||||
and (
|
||||
navigation_cluster == null
|
||||
or not navigation_cluster.is_ancestor_of(button)
|
||||
)
|
||||
):
|
||||
var group_key: Variant = (
|
||||
button.button_group
|
||||
if button.button_group != null
|
||||
else button.get_parent()
|
||||
)
|
||||
if not grouped_buttons.has(group_key):
|
||||
grouped_buttons[group_key] = []
|
||||
var buttons := grouped_buttons[group_key] as Array
|
||||
buttons.append(button)
|
||||
_collect_visible_toggle_buttons(
|
||||
child, navigation_cluster, grouped_buttons
|
||||
)
|
||||
|
||||
|
||||
func _handle_direct_page_shortcut(event: InputEvent) -> bool:
|
||||
var key_event := event as InputEventKey
|
||||
if (
|
||||
|
|
@ -772,6 +1237,7 @@ func open_menu() -> void:
|
|||
or not _fishing_spot.can_open_player_menu()
|
||||
):
|
||||
return
|
||||
_release_controller_ownership(false, true)
|
||||
_menu_generation += 1
|
||||
_transition_generation += 1
|
||||
_cancel_presentation_tween()
|
||||
|
|
@ -921,6 +1387,7 @@ func close_menu(
|
|||
if reason != CloseReason.USER:
|
||||
_finish_close(reason, restore_controls, _menu_generation)
|
||||
return
|
||||
_release_controller_ownership(false, true)
|
||||
get_viewport().gui_cancel_drag()
|
||||
_close_sale_confirmation()
|
||||
if reason in [
|
||||
|
|
@ -949,6 +1416,7 @@ func close_for_session_end() -> void:
|
|||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_release_controller_ownership(false, true)
|
||||
_cancel_presentation_tween()
|
||||
_cancel_page_tween()
|
||||
if visible:
|
||||
|
|
@ -1007,6 +1475,7 @@ func _show_section(section: Section) -> void:
|
|||
and _profile_page.request_close_confirmation()
|
||||
):
|
||||
return
|
||||
_release_controller_ownership(false, true)
|
||||
_begin_page_transition(section)
|
||||
|
||||
|
||||
|
|
@ -1122,22 +1591,39 @@ func _focus_current_section() -> void:
|
|||
if _shop_cooler_context_active:
|
||||
_focus_shop_cooler()
|
||||
return
|
||||
if _current_section == Section.COOLER:
|
||||
_cooler_sub_tab.grab_focus()
|
||||
elif _current_section == Section.BAG:
|
||||
_bag_sub_tab.grab_focus()
|
||||
elif _current_section == Section.TACKLE_BOX:
|
||||
_tackle_sub_tab.grab_focus()
|
||||
elif _current_section == Section.LOGBOOK:
|
||||
_catalog_logbook.focus_initial()
|
||||
elif _current_section == Section.NET:
|
||||
_the_net_page.focus_initial()
|
||||
elif _current_section == Section.MAIL:
|
||||
_mail_tab.grab_focus()
|
||||
elif _current_section == Section.PLAYERS:
|
||||
_players_tab.grab_focus()
|
||||
else:
|
||||
_profile_tab.grab_focus()
|
||||
_reserve_main_navigation_for_page_switching()
|
||||
_reserve_visible_secondary_navigation()
|
||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
||||
var candidates: Array[Control] = []
|
||||
_collect_focusable_content(self, navigation_cluster, candidates)
|
||||
if candidates.is_empty():
|
||||
return
|
||||
candidates.sort_custom(func(first: Control, second: Control) -> bool:
|
||||
if not is_equal_approx(first.global_position.y, second.global_position.y):
|
||||
return first.global_position.y < second.global_position.y
|
||||
return first.global_position.x < second.global_position.x
|
||||
)
|
||||
candidates[0].grab_focus()
|
||||
|
||||
|
||||
func _collect_focusable_content(
|
||||
root: Node,
|
||||
navigation_cluster: Control,
|
||||
output: Array[Control],
|
||||
) -> void:
|
||||
for child: Node in root.get_children():
|
||||
if child == navigation_cluster:
|
||||
continue
|
||||
var control := child as Control
|
||||
if control != null and not control.is_visible_in_tree():
|
||||
continue
|
||||
if (
|
||||
control != null
|
||||
and control.focus_mode != Control.FOCUS_NONE
|
||||
and not control is ScrollBar
|
||||
):
|
||||
output.append(control)
|
||||
_collect_focusable_content(child, navigation_cluster, output)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
|
|
@ -1161,6 +1647,7 @@ func _process(delta: float) -> void:
|
|||
|
||||
|
||||
func _configure_navigation_focus() -> void:
|
||||
_reserve_main_navigation_for_page_switching()
|
||||
var navigation: Array[BubbleButtonType] = [
|
||||
_inventory_tab,
|
||||
_logbook_tab,
|
||||
|
|
@ -1182,6 +1669,48 @@ func _configure_navigation_focus() -> void:
|
|||
bubble.focus_neighbor_right = bubble.get_path_to(next)
|
||||
bubble.focus_neighbor_top = bubble.focus_neighbor_left
|
||||
bubble.focus_neighbor_bottom = bubble.focus_neighbor_right
|
||||
|
||||
|
||||
func _reserve_main_navigation_for_page_switching() -> void:
|
||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
||||
if navigation_cluster == null:
|
||||
return
|
||||
_set_descendant_focus_disabled(navigation_cluster)
|
||||
|
||||
|
||||
func _set_descendant_focus_disabled(root: Node) -> void:
|
||||
for child: Node in root.get_children():
|
||||
var control := child as Control
|
||||
if control != null:
|
||||
control.focus_mode = Control.FOCUS_NONE
|
||||
_set_descendant_focus_disabled(child)
|
||||
|
||||
|
||||
func _reserve_visible_secondary_navigation() -> void:
|
||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
||||
var grouped_buttons: Dictionary = {}
|
||||
_collect_visible_toggle_buttons(self, navigation_cluster, grouped_buttons)
|
||||
var selected_tabs: Array = []
|
||||
var selected_group_y: float = INF
|
||||
for group_value: Variant in grouped_buttons.keys():
|
||||
var buttons := grouped_buttons[group_value] as Array
|
||||
if buttons.size() < 2:
|
||||
continue
|
||||
var group_y: float = INF
|
||||
for item: Variant in buttons:
|
||||
var button := item as BaseButton
|
||||
group_y = minf(group_y, button.global_position.y)
|
||||
if group_y < selected_group_y:
|
||||
selected_tabs = buttons
|
||||
selected_group_y = group_y
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
var displaced_focus: bool = selected_tabs.has(focus_owner)
|
||||
for item: Variant in selected_tabs:
|
||||
var tab := item as Control
|
||||
tab.focus_mode = Control.FOCUS_NONE
|
||||
if displaced_focus:
|
||||
focus_owner.release_focus()
|
||||
call_deferred("_focus_current_section")
|
||||
var inventory_tabs: Array[Button] = [
|
||||
_cooler_sub_tab,
|
||||
_tackle_sub_tab,
|
||||
|
|
@ -1359,6 +1888,7 @@ func _refresh_tackle_box() -> void:
|
|||
row.text = "%s ×%d" % [item.display_name, owned.quantity]
|
||||
row.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
row.toggle_mode = true
|
||||
row.set_meta(&"controller_tackle_item_id", owned.item_id)
|
||||
row.button_pressed = owned.item_id == _selected_tackle_item_id
|
||||
if item.is_bait() and item.icon != null:
|
||||
_apply_tackle_bait_button_style(row)
|
||||
|
|
|
|||
|
|
@ -336,7 +336,32 @@ func _confirm(text: String, action: Callable) -> void:
|
|||
|
||||
|
||||
func _focus_first() -> void:
|
||||
for child: Node in _tabs.get_children():
|
||||
if child is Button and child.visible and not child.disabled:
|
||||
child.grab_focus()
|
||||
var candidates: Array[Control] = []
|
||||
_collect_focusable_player_controls(self, candidates)
|
||||
if candidates.is_empty():
|
||||
return
|
||||
candidates.sort_custom(func(first: Control, second: Control) -> bool:
|
||||
if not is_equal_approx(first.global_position.y, second.global_position.y):
|
||||
return first.global_position.y < second.global_position.y
|
||||
return first.global_position.x < second.global_position.x
|
||||
)
|
||||
candidates[0].grab_focus()
|
||||
|
||||
|
||||
func _collect_focusable_player_controls(
|
||||
root: Node,
|
||||
output: Array[Control],
|
||||
) -> void:
|
||||
for child: Node in root.get_children():
|
||||
if child == _tabs:
|
||||
continue
|
||||
var control := child as Control
|
||||
if control != null and not control.is_visible_in_tree():
|
||||
continue
|
||||
if (
|
||||
control != null
|
||||
and control.focus_mode != Control.FOCUS_NONE
|
||||
and not control is ScrollBar
|
||||
):
|
||||
output.append(control)
|
||||
_collect_focusable_player_controls(child, output)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
class_name SettingsBubblePage
|
||||
extends Control
|
||||
|
||||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
|
||||
@export var page_id: StringName
|
||||
@export var cluster_path: NodePath = ^"BubbleCluster"
|
||||
@export var bubble_paths: Array[NodePath] = []
|
||||
|
|
@ -169,23 +173,13 @@ func _update_layout() -> void:
|
|||
_cluster.position = _resting_cluster_position
|
||||
_cluster.size = field_size
|
||||
_cluster.apply_layout(field_size, compact)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_bubbles)
|
||||
|
||||
|
||||
func _configure_focus_order() -> void:
|
||||
for bubble: BubbleButton in _bubbles:
|
||||
bubble.focus_mode = Control.FOCUS_NONE
|
||||
for index: int in _focus_bubbles.size():
|
||||
var bubble: BubbleButton = _focus_bubbles[index]
|
||||
if index > 0:
|
||||
bubble.focus_neighbor_top = bubble.get_path_to(
|
||||
_focus_bubbles[index - 1]
|
||||
)
|
||||
bubble.focus_neighbor_left = bubble.focus_neighbor_top
|
||||
if index + 1 < _focus_bubbles.size():
|
||||
bubble.focus_neighbor_bottom = bubble.get_path_to(
|
||||
_focus_bubbles[index + 1]
|
||||
)
|
||||
bubble.focus_neighbor_right = bubble.focus_neighbor_bottom
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_bubbles)
|
||||
|
||||
|
||||
func _set_interactive(interactive: bool) -> void:
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ enum PresentationMode {
|
|||
@onready var _mouse_value: BubbleButton = %MouseValue
|
||||
@onready var _controller_value: BubbleButton = %ControllerValue
|
||||
@onready var _invert_y_toggle: BubbleButton = %InvertYToggle
|
||||
@onready var _on_screen_keyboard_toggle: BubbleButton = %OnScreenKeyboardToggle
|
||||
@onready var _auto_click_toggle: BubbleButton = %AutoClickToggle
|
||||
@onready var _auto_click_interval: BubbleButton = %AutoClickIntervalValue
|
||||
|
||||
|
|
@ -84,6 +85,7 @@ var _auto_click_interval_value: float = 0.20
|
|||
var _mouse_sensitivity: float = 0.005
|
||||
var _controller_sensitivity: float = 2.5
|
||||
var _invert_camera_y: bool = false
|
||||
var _on_screen_keyboard_enabled: bool = false
|
||||
var _network_profile: NetworkProfilePreferences
|
||||
var _network_session: NetworkSession
|
||||
var _data_root: PlayerDataRoot
|
||||
|
|
@ -161,6 +163,7 @@ func _ready() -> void:
|
|||
_adjust_controller_sensitivity.bind(1)
|
||||
)
|
||||
_invert_y_toggle.pressed.connect(_toggle_invert_y)
|
||||
_on_screen_keyboard_toggle.pressed.connect(_toggle_on_screen_keyboard)
|
||||
_auto_click_toggle.pressed.connect(_toggle_auto_click)
|
||||
for control: Control in [_auto_click_toggle, _auto_click_interval]:
|
||||
control.mouse_entered.connect(
|
||||
|
|
@ -704,6 +707,7 @@ func _apply_settings() -> void:
|
|||
edited.mouse_camera_sensitivity = _mouse_sensitivity
|
||||
edited.controller_camera_sensitivity = _controller_sensitivity
|
||||
edited.invert_camera_y = _invert_camera_y
|
||||
edited.on_screen_keyboard_enabled = _on_screen_keyboard_enabled
|
||||
if _settings_manager.apply_settings(edited):
|
||||
if (
|
||||
_presentation_mode == PresentationMode.TITLE_EMBEDDED
|
||||
|
|
@ -725,6 +729,7 @@ func _load_controls() -> void:
|
|||
_mouse_sensitivity = settings.mouse_camera_sensitivity
|
||||
_controller_sensitivity = settings.controller_camera_sensitivity
|
||||
_invert_camera_y = settings.invert_camera_y
|
||||
_on_screen_keyboard_enabled = settings.on_screen_keyboard_enabled
|
||||
_refresh_value_labels()
|
||||
|
||||
|
||||
|
|
@ -755,6 +760,10 @@ func _refresh_value_labels() -> void:
|
|||
"invert\nvertical\ncamera\n"
|
||||
+ ("on" if _invert_camera_y else "off")
|
||||
)
|
||||
_on_screen_keyboard_toggle.text = (
|
||||
"on-screen\nkeyboard\n"
|
||||
+ ("on" if _on_screen_keyboard_enabled else "off")
|
||||
)
|
||||
_auto_click_toggle.text = (
|
||||
"accessibility\nauto-click\n"
|
||||
+ ("on" if _auto_click_enabled else "off")
|
||||
|
|
@ -883,6 +892,11 @@ func _toggle_invert_y() -> void:
|
|||
_refresh_value_labels()
|
||||
|
||||
|
||||
func _toggle_on_screen_keyboard() -> void:
|
||||
_on_screen_keyboard_enabled = not _on_screen_keyboard_enabled
|
||||
_refresh_value_labels()
|
||||
|
||||
|
||||
func _toggle_auto_click() -> void:
|
||||
_auto_click_enabled = not _auto_click_enabled
|
||||
_refresh_value_labels()
|
||||
|
|
|
|||
|
|
@ -397,8 +397,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/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")])
|
||||
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/OnScreenKeyboardToggle"), 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/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
initial_focus_path = NodePath("BubbleCluster/MouseValue")
|
||||
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
||||
|
||||
|
|
@ -491,17 +491,29 @@ neutral_size = Vector2(176, 170)
|
|||
compact_minimum_size = Vector2(170, 164)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 24
|
||||
desktop_anchor = Vector2(525, 145)
|
||||
compact_anchor = Vector2(485, 135)
|
||||
desktop_anchor = Vector2(525, 105)
|
||||
compact_anchor = Vector2(485, 95)
|
||||
motion_phase = 4.5
|
||||
|
||||
[node name="OnScreenKeyboardToggle" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "on-screen\nkeyboard\noff"
|
||||
neutral_size = Vector2(170, 150)
|
||||
compact_minimum_size = Vector2(154, 136)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 22
|
||||
desktop_anchor = Vector2(525, 255)
|
||||
compact_anchor = Vector2(485, 235)
|
||||
motion_phase = 4.7
|
||||
|
||||
[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)
|
||||
desktop_anchor = Vector2(525, 390)
|
||||
compact_anchor = Vector2(485, 355)
|
||||
compact_minimum_size = Vector2(148, 138)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 23
|
||||
|
|
@ -512,8 +524,8 @@ unique_name_in_owner = true
|
|||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "back"
|
||||
neutral_size = Vector2(100, 94)
|
||||
desktop_anchor = Vector2(635, 445)
|
||||
compact_anchor = Vector2(555, 405)
|
||||
desktop_anchor = Vector2(635, 470)
|
||||
compact_anchor = Vector2(555, 420)
|
||||
compact_minimum_size = Vector2(84, 80)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 17
|
||||
|
|
|
|||
|
|
@ -608,6 +608,10 @@ func _prepare_awaiting_start_input() -> void:
|
|||
_navigation_focus_active = false
|
||||
_modal_restore_navigation_focus = false
|
||||
_button_center.show()
|
||||
var bubble_field := get_node_or_null("%BubbleField") as Control
|
||||
if bubble_field != null:
|
||||
bubble_field.hide()
|
||||
call_deferred("set_process", visible)
|
||||
_feedback_label.show()
|
||||
_feedback_label.modulate.a = 0.0
|
||||
_continue_stats_hovered = false
|
||||
|
|
@ -650,6 +654,10 @@ func _reveal_primary_menu() -> void:
|
|||
):
|
||||
return
|
||||
_awaiting_start_input = false
|
||||
_button_center.show()
|
||||
var bubble_field := get_node_or_null("%BubbleField") as Control
|
||||
if bubble_field != null:
|
||||
bubble_field.show()
|
||||
_stop_entry_prompt_animation()
|
||||
_title_entry_generation += 1
|
||||
_title_entry_transition_active = true
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ const MIN_UI_VIEWPORT_SIZE: Vector2i = Vector2i(256, 180)
|
|||
const UIReferencePresentationType = preload(
|
||||
"res://ui/ui_reference_presentation.gd"
|
||||
)
|
||||
const ControllerFocusPresentationType = preload(
|
||||
"res://ui/controller_focus_presentation.gd"
|
||||
)
|
||||
const ControllerFocusRecoveryType = preload(
|
||||
"res://ui/controller_focus_recovery.gd"
|
||||
)
|
||||
const OnScreenKeyboardType = preload("res://ui/on_screen_keyboard.gd")
|
||||
|
||||
signal effective_pixel_size_changed(
|
||||
requested_pixel_size: int,
|
||||
|
|
@ -16,6 +23,9 @@ signal effective_pixel_size_changed(
|
|||
@onready var _canonical_stage: Control = (
|
||||
$UIViewport/GameUI/UIRoot/CanonicalStage
|
||||
)
|
||||
@onready var _hotbar: HotbarUI = (
|
||||
$UIViewport/GameUI/UIRoot/CanonicalStage/Hotbar
|
||||
)
|
||||
@onready var _chat_ui: ChatUI = $UIViewport/GameUI/UIRoot/ChatUI
|
||||
@onready var _title_content_stage: Control = (
|
||||
$UIViewport/GameUI/UIRoot/TitleScreen/ResponsiveTitleStage
|
||||
|
|
@ -26,14 +36,25 @@ var _effective_pixel_size: int = PlayerSettings.DEFAULT_UI_PIXEL_SIZE
|
|||
var _gameplay_active: bool = false
|
||||
var _interactive_ui_open: bool = false
|
||||
var _passive_pointer_ui_enabled: bool = false
|
||||
var _on_screen_keyboard: OnScreenKeyboardType
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_on_screen_keyboard = OnScreenKeyboardType.new()
|
||||
_ui_root.add_child(_on_screen_keyboard)
|
||||
var controller_focus_recovery := ControllerFocusRecoveryType.new()
|
||||
_ui_root.add_child(controller_focus_recovery)
|
||||
var controller_focus_presentation := ControllerFocusPresentationType.new()
|
||||
_ui_root.add_child(controller_focus_presentation)
|
||||
var root_viewport: Viewport = get_viewport()
|
||||
root_viewport.size_changed.connect(_resize_presentation)
|
||||
_resize_presentation()
|
||||
|
||||
|
||||
func set_on_screen_keyboard_enabled(enabled: bool) -> void:
|
||||
_on_screen_keyboard.set_enabled(enabled)
|
||||
|
||||
|
||||
func set_pixel_size(pixel_size: int) -> void:
|
||||
_requested_pixel_size = clampi(
|
||||
pixel_size,
|
||||
|
|
@ -120,6 +141,7 @@ func _resize_presentation() -> void:
|
|||
UIReferencePresentationType.get_stage_position(display_size)
|
||||
)
|
||||
_canonical_stage.size = UIReferencePresentationType.REFERENCE_SIZE
|
||||
_hotbar.position = Vector2(0.0, _canonical_stage.position.y)
|
||||
_title_content_stage.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
_title_content_stage.position = _canonical_stage.position
|
||||
_title_content_stage.size = UIReferencePresentationType.REFERENCE_SIZE
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue