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(
|
_shop_interaction.local_player_range_changed.connect(
|
||||||
_on_shop_range_changed
|
_on_shop_range_changed
|
||||||
)
|
)
|
||||||
|
_game_ui.set_shop_npc_player_in_range(
|
||||||
|
_shop_interaction.is_local_player_in_range()
|
||||||
|
)
|
||||||
_save_manager.setup(
|
_save_manager.setup(
|
||||||
_player.inventory,
|
_player.inventory,
|
||||||
_player.collection_log,
|
_player.collection_log,
|
||||||
|
|
@ -1119,6 +1122,9 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
||||||
return
|
return
|
||||||
_apply_world_pixelation(settings.world_pixel_size)
|
_apply_world_pixelation(settings.world_pixel_size)
|
||||||
_ui_pixelation.set_pixel_size(settings.ui_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(
|
_game_ui.get_title_screen().set_world_pixelation(
|
||||||
settings.world_pixel_size
|
settings.world_pixel_size
|
||||||
)
|
)
|
||||||
|
|
@ -1141,7 +1147,11 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
||||||
func _apply_world_pixelation(pixel_size: int) -> void:
|
func _apply_world_pixelation(pixel_size: int) -> void:
|
||||||
var root_viewport: Viewport = get_viewport()
|
var root_viewport: Viewport = get_viewport()
|
||||||
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
|
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.msaa_3d = Viewport.MSAA_DISABLED
|
||||||
root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
|
root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
|
||||||
root_viewport.use_taa = false
|
root_viewport.use_taa = false
|
||||||
|
|
@ -1670,6 +1680,7 @@ func _exit_tree() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_shop_range_changed(in_range: bool) -> void:
|
func _on_shop_range_changed(in_range: bool) -> void:
|
||||||
|
_game_ui.set_shop_npc_player_in_range(in_range)
|
||||||
if not in_range:
|
if not in_range:
|
||||||
_game_ui.get_fishing_shop().close_for_range_exit()
|
_game_ui.get_fishing_shop().close_for_range_exit()
|
||||||
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
|
_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.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_range(0.5, 5.0, 0.1) var controller_camera_sensitivity: float = 2.5
|
||||||
@export var invert_camera_y: bool = false
|
@export var invert_camera_y: bool = false
|
||||||
|
@export var on_screen_keyboard_enabled: bool = false
|
||||||
@export var chat_draft: String = ""
|
@export var chat_draft: String = ""
|
||||||
@export var chat_collapsed: bool = false
|
@export var chat_collapsed: bool = false
|
||||||
@export var chat_dock_right: bool = false
|
@export var chat_dock_right: bool = false
|
||||||
|
|
@ -63,6 +64,7 @@ func copy() -> PlayerSettings:
|
||||||
result.mouse_camera_sensitivity = mouse_camera_sensitivity
|
result.mouse_camera_sensitivity = mouse_camera_sensitivity
|
||||||
result.controller_camera_sensitivity = controller_camera_sensitivity
|
result.controller_camera_sensitivity = controller_camera_sensitivity
|
||||||
result.invert_camera_y = invert_camera_y
|
result.invert_camera_y = invert_camera_y
|
||||||
|
result.on_screen_keyboard_enabled = on_screen_keyboard_enabled
|
||||||
result.chat_draft = chat_draft
|
result.chat_draft = chat_draft
|
||||||
result.chat_collapsed = chat_collapsed
|
result.chat_collapsed = chat_collapsed
|
||||||
result.chat_dock_right = chat_dock_right
|
result.chat_dock_right = chat_dock_right
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@ func load_settings() -> bool:
|
||||||
if (
|
if (
|
||||||
typeof(accessibility.get("auto_click_enabled")) != TYPE_BOOL
|
typeof(accessibility.get("auto_click_enabled")) != TYPE_BOOL
|
||||||
or typeof(camera.get("invert_vertical")) != 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 (
|
or (
|
||||||
accessibility.has("use_readable_interface_font")
|
accessibility.has("use_readable_interface_font")
|
||||||
and typeof(accessibility["use_readable_interface_font"]) != TYPE_BOOL
|
and typeof(accessibility["use_readable_interface_font"]) != TYPE_BOOL
|
||||||
|
|
@ -88,6 +92,9 @@ func load_settings() -> bool:
|
||||||
-1.0
|
-1.0
|
||||||
)
|
)
|
||||||
loaded.invert_camera_y = camera["invert_vertical"]
|
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(
|
loaded.world_pixel_size = _read_clamped_integer(
|
||||||
presentation.get(
|
presentation.get(
|
||||||
"world_pixel_size",
|
"world_pixel_size",
|
||||||
|
|
@ -176,6 +183,9 @@ func save_now() -> bool:
|
||||||
"auto_click_enabled": current_settings.auto_click_enabled,
|
"auto_click_enabled": current_settings.auto_click_enabled,
|
||||||
"auto_click_interval": current_settings.auto_click_interval,
|
"auto_click_interval": current_settings.auto_click_interval,
|
||||||
"use_readable_interface_font": true,
|
"use_readable_interface_font": true,
|
||||||
|
"on_screen_keyboard_enabled": (
|
||||||
|
current_settings.on_screen_keyboard_enabled
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"camera": {
|
"camera": {
|
||||||
"mouse_sensitivity": current_settings.mouse_camera_sensitivity,
|
"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(
|
assert(player_menu.size.is_equal_approx(
|
||||||
UIReferencePresentationType.REFERENCE_SIZE
|
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(
|
assert(hotbar.size.is_equal_approx(
|
||||||
UIReferencePresentationType.REFERENCE_SIZE
|
UIReferencePresentationType.REFERENCE_SIZE
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -568,6 +568,21 @@ func _refresh_input_ownership() -> void:
|
||||||
)
|
)
|
||||||
_panel.mouse_filter = filter
|
_panel.mouse_filter = filter
|
||||||
_history.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:
|
func _clock_panel_style() -> StyleBoxFlat:
|
||||||
|
|
@ -886,9 +901,14 @@ func _refresh_visibility() -> void:
|
||||||
_collapse_button.show()
|
_collapse_button.show()
|
||||||
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
||||||
_panel.show()
|
_panel.show()
|
||||||
|
_collapse_button.focus_mode = (
|
||||||
|
Control.FOCUS_ALL if _opened else Control.FOCUS_NONE
|
||||||
|
)
|
||||||
_height_button.visible = not _mobile_mode
|
_height_button.visible = not _mobile_mode
|
||||||
_height_button.focus_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
|
_unread_indicator.visible = collapsed and _collapsed_has_unread
|
||||||
_hint.visible = not _opened
|
_hint.visible = not _opened
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ var _base_position: Vector2 = Vector2.ZERO
|
||||||
var _presented_size: Vector2 = Vector2.ZERO
|
var _presented_size: Vector2 = Vector2.ZERO
|
||||||
var _compact: bool = false
|
var _compact: bool = false
|
||||||
var _presentation_initialized: bool = false
|
var _presentation_initialized: bool = false
|
||||||
|
var _controller_preview_active: bool = false
|
||||||
|
var _controller_preview_texture: Texture2D
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -146,6 +148,15 @@ func set_drag_enabled(enabled: bool) -> void:
|
||||||
_hovered = false
|
_hovered = false
|
||||||
|
|
||||||
|
|
||||||
|
func set_controller_placement_preview(
|
||||||
|
active: bool,
|
||||||
|
texture: Texture2D,
|
||||||
|
) -> void:
|
||||||
|
_controller_preview_active = active
|
||||||
|
_controller_preview_texture = texture
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
func refresh() -> void:
|
func refresh() -> void:
|
||||||
if _hotbar == null:
|
if _hotbar == null:
|
||||||
return
|
return
|
||||||
|
|
@ -161,11 +172,16 @@ func refresh() -> void:
|
||||||
if _fish_inventory != null and not catch_id.is_empty()
|
if _fish_inventory != null and not catch_id.is_empty()
|
||||||
else null
|
else null
|
||||||
)
|
)
|
||||||
_item_icon.texture = (
|
var assigned_texture: Texture2D = (
|
||||||
fish_catch.fish.display_texture
|
fish_catch.fish.display_texture
|
||||||
if fish_catch != null
|
if fish_catch != null
|
||||||
else item.icon if item != null else 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 = (
|
var quantity: int = (
|
||||||
_bag.get_quantity(item_id)
|
_bag.get_quantity(item_id)
|
||||||
if _bag != null and not item_id.is_empty()
|
if _bag != null and not item_id.is_empty()
|
||||||
|
|
@ -177,7 +193,9 @@ func refresh() -> void:
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
_quantity_label.text = quantity_text
|
_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 = (
|
tooltip_text = (
|
||||||
"%s · %.1f lb" % [
|
"%s · %.1f lb" % [
|
||||||
FishQualityType.qualified_name(
|
FishQualityType.qualified_name(
|
||||||
|
|
@ -192,7 +210,11 @@ func refresh() -> void:
|
||||||
var was_selected: bool = _selected
|
var was_selected: bool = _selected
|
||||||
var was_empty: bool = _empty
|
var was_empty: bool = _empty
|
||||||
_selected = slot_index == _hotbar.get_selected_slot()
|
_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:
|
if was_selected != _selected or was_empty != _empty:
|
||||||
_apply_style()
|
_apply_style()
|
||||||
|
|
||||||
|
|
@ -220,7 +242,9 @@ func _apply_style() -> void:
|
||||||
var normal_fill: Color = profile.normal_fill
|
var normal_fill: Color = profile.normal_fill
|
||||||
normal_fill.a = 1.0
|
normal_fill.a = 1.0
|
||||||
var selected_fill: Color = normal_fill
|
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)
|
selected_fill = normal_fill.lightened(0.12)
|
||||||
add_theme_stylebox_override(
|
add_theme_stylebox_override(
|
||||||
"normal",
|
"normal",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,53 @@ func _ready() -> void:
|
||||||
_apply_icon_presentation(neutral_size)
|
_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(
|
func apply_layout(
|
||||||
center: Vector2,
|
center: Vector2,
|
||||||
bubble_size: Vector2,
|
bubble_size: Vector2,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
class_name BubbleCluster
|
class_name BubbleCluster
|
||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
|
const ControllerFocusNavigationType = preload(
|
||||||
|
"res://ui/controller_focus_navigation.gd"
|
||||||
|
)
|
||||||
|
|
||||||
@export var profile: BubbleMenuProfile
|
@export var profile: BubbleMenuProfile
|
||||||
@export var desktop_reference_size: Vector2 = Vector2(396.0, 318.0)
|
@export var desktop_reference_size: Vector2 = Vector2(396.0, 318.0)
|
||||||
@export var compact_reference_size: Vector2 = Vector2(294.0, 200.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:
|
if bubble.profile == null:
|
||||||
bubble.profile = profile
|
bubble.profile = profile
|
||||||
bubble.apply_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:
|
func apply_layout(field_size: Vector2, compact: bool) -> void:
|
||||||
|
|
@ -52,6 +48,7 @@ func apply_layout(field_size: Vector2, compact: bool) -> void:
|
||||||
bubble_size,
|
bubble_size,
|
||||||
profile.font_size_ratio
|
profile.font_size_ratio
|
||||||
)
|
)
|
||||||
|
ControllerFocusNavigationType.configure_spatial_neighbors(_bubbles)
|
||||||
|
|
||||||
|
|
||||||
func advance_motion(delta: float) -> void:
|
func advance_motion(delta: float) -> void:
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ var _quality_color := Color.WHITE
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
set_meta(&"controller_focus_inversion_disabled", true)
|
||||||
resized.connect(_update_visual_pivot)
|
resized.connect(_update_visual_pivot)
|
||||||
focus_entered.connect(_refresh_style)
|
focus_entered.connect(_refresh_style)
|
||||||
focus_exited.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_key: Label = %ShopPromptKey
|
||||||
@onready var _shop_prompt_pointer: Polygon2D = %ShopPromptPointer
|
@onready var _shop_prompt_pointer: Polygon2D = %ShopPromptPointer
|
||||||
var _shop_animalese_voice: AnimaleseVoiceType
|
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 _effect_status: Label = %EffectStatus
|
||||||
@onready var _chat_ui: ChatUIType = %ChatUI
|
@onready var _chat_ui: ChatUIType = %ChatUI
|
||||||
@onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu
|
@onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu
|
||||||
|
|
@ -207,6 +209,12 @@ func _ready() -> void:
|
||||||
_hotbar_ui.presentation_transition_finished.connect(
|
_hotbar_ui.presentation_transition_finished.connect(
|
||||||
_on_hotbar_presentation_transition_finished
|
_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(
|
_title_settings_panel.panel_visibility_changed.connect(
|
||||||
_on_settings_visibility_changed
|
_on_settings_visibility_changed
|
||||||
)
|
)
|
||||||
|
|
@ -503,7 +511,7 @@ func _handle_controller_chat_controls(event: InputEvent) -> bool:
|
||||||
_chat_ui.toggle_chat()
|
_chat_ui.toggle_chat()
|
||||||
return true
|
return true
|
||||||
if focus_pressed:
|
if focus_pressed:
|
||||||
_chat_ui.toggle_focus()
|
_chat_ui.refocus_gameplay()
|
||||||
return true
|
return true
|
||||||
if accept_pressed:
|
if accept_pressed:
|
||||||
return _chat_ui.request_virtual_keyboard()
|
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:
|
func set_gameplay_ui_enabled(enabled: bool) -> void:
|
||||||
_gameplay_ui_enabled = enabled
|
_gameplay_ui_enabled = enabled
|
||||||
|
if enabled:
|
||||||
|
_try_start_shop_npc_speech()
|
||||||
_gameplay_transient_hud.visible = enabled and not _player_menu_open
|
_gameplay_transient_hud.visible = enabled and not _player_menu_open
|
||||||
_experience_presentation.visible = enabled
|
_experience_presentation.visible = enabled
|
||||||
_refresh_chat_availability()
|
_refresh_chat_availability()
|
||||||
|
|
@ -1362,7 +1372,6 @@ func set_shop_prompt_visible(
|
||||||
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
|
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
|
||||||
) -> void:
|
) -> void:
|
||||||
_apply_shop_prompt_style()
|
_apply_shop_prompt_style()
|
||||||
var was_visible := _shop_prompt.visible
|
|
||||||
_shop_prompt.visible = (
|
_shop_prompt.visible = (
|
||||||
is_visible
|
is_visible
|
||||||
and _gameplay_ui_enabled
|
and _gameplay_ui_enabled
|
||||||
|
|
@ -1371,23 +1380,42 @@ func set_shop_prompt_visible(
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
)
|
)
|
||||||
if _shop_prompt.visible:
|
if _shop_prompt.visible:
|
||||||
if not was_visible:
|
|
||||||
_ensure_shop_animalese_voice()
|
|
||||||
TypewriterRevealType.start(
|
|
||||||
_shop_prompt_message,
|
|
||||||
SHOP_SPEECH_CHARACTERS_PER_SECOND,
|
|
||||||
)
|
|
||||||
_shop_animalese_voice.speak_text(
|
|
||||||
_shop_prompt_message,
|
|
||||||
_shop_prompt_message.text,
|
|
||||||
"shopkeeper",
|
|
||||||
SHOP_ANIMALESE_VOICE_ID,
|
|
||||||
SHOP_SPEECH_CHARACTERS_PER_SECOND,
|
|
||||||
)
|
|
||||||
if world_anchor.is_finite():
|
if world_anchor.is_finite():
|
||||||
_position_shop_prompt(world_anchor)
|
_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,
|
||||||
|
SHOP_SPEECH_CHARACTERS_PER_SECOND,
|
||||||
|
)
|
||||||
|
_shop_animalese_voice.speak_text(
|
||||||
|
_shop_prompt_message,
|
||||||
|
_shop_prompt_message.text,
|
||||||
|
"shopkeeper",
|
||||||
|
SHOP_ANIMALESE_VOICE_ID,
|
||||||
|
SHOP_SPEECH_CHARACTERS_PER_SECOND,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _ensure_shop_animalese_voice() -> void:
|
func _ensure_shop_animalese_voice() -> void:
|
||||||
if _shop_animalese_voice != null:
|
if _shop_animalese_voice != null:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
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 _motion_elapsed: float = 0.0
|
||||||
var _compact_layout: bool = false
|
var _compact_layout: bool = false
|
||||||
var _player_menu_context: 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_tween: Tween
|
||||||
var _visibility_generation: int = 0
|
var _visibility_generation: int = 0
|
||||||
|
|
||||||
|
|
@ -99,6 +105,79 @@ func set_drag_enabled(enabled: bool) -> void:
|
||||||
_hide_item_name()
|
_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:
|
func set_player_menu_context(enabled: bool) -> void:
|
||||||
if _player_menu_context == enabled:
|
if _player_menu_context == enabled:
|
||||||
return
|
return
|
||||||
|
|
@ -230,6 +309,9 @@ func _collect_slots() -> void:
|
||||||
slot.item_hover_ended.connect(_on_slot_item_hover_ended)
|
slot.item_hover_ended.connect(_on_slot_item_hover_ended)
|
||||||
slot.item_drag_started.connect(_on_slot_drag_started)
|
slot.item_drag_started.connect(_on_slot_drag_started)
|
||||||
slot.item_drag_finished.connect(_on_slot_drag_finished)
|
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.append(slot)
|
||||||
_slots.sort_custom(
|
_slots.sort_custom(
|
||||||
func(
|
func(
|
||||||
|
|
@ -278,10 +360,20 @@ func _on_selected_slot_changed(
|
||||||
_item_id: StringName,
|
_item_id: StringName,
|
||||||
) -> void:
|
) -> void:
|
||||||
_refresh()
|
_refresh()
|
||||||
|
if _controller_placement_active:
|
||||||
|
_refresh_controller_placement_preview()
|
||||||
|
return
|
||||||
if _hovered_slot_index < 0:
|
if _hovered_slot_index < 0:
|
||||||
_show_selected_item_briefly()
|
_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(
|
func _on_slot_item_hovered(
|
||||||
slot_index: int,
|
slot_index: int,
|
||||||
item_id: StringName,
|
item_id: StringName,
|
||||||
|
|
|
||||||
|
|
@ -453,6 +453,7 @@ func _refresh_catalog() -> void:
|
||||||
|
|
||||||
func _make_entry(fish: FishDataType, discovered: bool) -> Button:
|
func _make_entry(fish: FishDataType, discovered: bool) -> Button:
|
||||||
var entry := Button.new()
|
var entry := Button.new()
|
||||||
|
entry.set_meta(&"controller_focus_inversion_disabled", true)
|
||||||
entry.custom_minimum_size = CATALOG_ENTRY_SIZE
|
entry.custom_minimum_size = CATALOG_ENTRY_SIZE
|
||||||
entry.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
entry.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
entry.size_flags_vertical = 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 menu_visibility_changed(is_open: bool)
|
||||||
signal inventory_hotbar_context_changed(show_hotbar: 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 menu_exit_started
|
||||||
signal shop_cooler_modal_changed(is_open: bool)
|
signal shop_cooler_modal_changed(is_open: bool)
|
||||||
|
|
||||||
|
|
@ -124,6 +130,12 @@ enum CloseReason {
|
||||||
TEARDOWN,
|
TEARDOWN,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ControllerOwnership {
|
||||||
|
ITEM_LIST,
|
||||||
|
NOTEPAD_ACTIONS,
|
||||||
|
HOTBAR_PLACEMENT,
|
||||||
|
}
|
||||||
|
|
||||||
const INVENTORY_MAIN_POSITION := Vector2(54.0, 166.0)
|
const INVENTORY_MAIN_POSITION := Vector2(54.0, 166.0)
|
||||||
const INVENTORY_MAIN_SIZE := Vector2(882.0, 484.0)
|
const INVENTORY_MAIN_SIZE := Vector2(882.0, 484.0)
|
||||||
const INVENTORY_PANEL_GAP := 16.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 _bag_view: BagView = BagView.EQUIPMENT
|
||||||
var _tackle_view: TackleView = TackleView.BAIT
|
var _tackle_view: TackleView = TackleView.BAIT
|
||||||
var _selected_tackle_item_id: StringName
|
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_mode: SortMode = SortMode.CATCH_ORDER
|
||||||
var _sort_descending: bool = true
|
var _sort_descending: bool = true
|
||||||
var _fish_selection := FishBatchSelectionType.new()
|
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)
|
_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:
|
func _input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey and event.echo:
|
if event is InputEventKey and event.echo:
|
||||||
return
|
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):
|
if _handle_controller_page_switch(event):
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
if _handle_controller_secondary_switch(event):
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
if _handle_direct_page_shortcut(event):
|
if _handle_direct_page_shortcut(event):
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
|
@ -613,47 +647,360 @@ func _input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
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:
|
func _handle_controller_page_switch(event: InputEvent) -> bool:
|
||||||
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
|
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
|
||||||
|
var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
|
||||||
var use_mapping: bool = (
|
var use_mapping: bool = (
|
||||||
_controller_mapping_manager != null
|
_controller_mapping_manager != null
|
||||||
)
|
)
|
||||||
var uses_left_bumper: bool = (
|
var uses_left_trigger: bool = (
|
||||||
_controller_mapping_manager.event_uses_role(
|
_controller_mapping_manager.event_uses_role(
|
||||||
event, ControllerMappingManagerType.ROLE_LB
|
event, ControllerMappingManagerType.ROLE_POINTER_MODIFIER
|
||||||
)
|
)
|
||||||
if use_mapping
|
if use_mapping
|
||||||
else (
|
else (
|
||||||
button_event != null
|
motion_event != null
|
||||||
and button_event.button_index == JOY_BUTTON_LEFT_SHOULDER
|
and motion_event.axis == JOY_AXIS_TRIGGER_LEFT
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
var uses_right_bumper: bool = (
|
var uses_right_trigger: bool = (
|
||||||
_controller_mapping_manager.event_uses_role(
|
_controller_mapping_manager.event_uses_role(
|
||||||
event, ControllerMappingManagerType.ROLE_RB
|
event, ControllerMappingManagerType.ROLE_CAMERA_ZOOM
|
||||||
)
|
)
|
||||||
if use_mapping
|
if use_mapping
|
||||||
else (
|
else (
|
||||||
button_event != null
|
motion_event != null
|
||||||
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
|
and motion_event.axis == JOY_AXIS_TRIGGER_RIGHT
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
button_event == null
|
not (uses_left_trigger or uses_right_trigger)
|
||||||
or not (uses_left_bumper or uses_right_bumper)
|
|
||||||
or not visible
|
or not visible
|
||||||
):
|
):
|
||||||
return false
|
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
|
return true
|
||||||
# Shoulder input belongs to the Player Menu while it is visible, even when
|
if (
|
||||||
# a transition or modal temporarily prevents changing pages. This keeps LB
|
(uses_left_trigger and _left_page_trigger_held)
|
||||||
# from opening Chat and RB from leaking into gameplay behind the menu.
|
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 (
|
if (
|
||||||
_transitioning
|
_transitioning
|
||||||
or _page_transitioning
|
or _page_transitioning
|
||||||
or _sale_confirmation.visible
|
or _sale_confirmation.visible
|
||||||
or get_viewport().gui_is_dragging()
|
or get_viewport().gui_is_dragging()
|
||||||
|
or _controller_ownership != ControllerOwnership.ITEM_LIST
|
||||||
):
|
):
|
||||||
return true
|
return true
|
||||||
var sections: Array[Section] = [
|
var sections: Array[Section] = [
|
||||||
|
|
@ -670,13 +1017,131 @@ func _handle_controller_page_switch(event: InputEvent) -> bool:
|
||||||
if current_index < 0:
|
if current_index < 0:
|
||||||
current_index = 0
|
current_index = 0
|
||||||
var direction: int = (
|
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())
|
var next_index: int = wrapi(current_index + direction, 0, sections.size())
|
||||||
_show_section(sections[next_index])
|
_show_section(sections[next_index])
|
||||||
return true
|
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:
|
func _handle_direct_page_shortcut(event: InputEvent) -> bool:
|
||||||
var key_event := event as InputEventKey
|
var key_event := event as InputEventKey
|
||||||
if (
|
if (
|
||||||
|
|
@ -772,6 +1237,7 @@ func open_menu() -> void:
|
||||||
or not _fishing_spot.can_open_player_menu()
|
or not _fishing_spot.can_open_player_menu()
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
_release_controller_ownership(false, true)
|
||||||
_menu_generation += 1
|
_menu_generation += 1
|
||||||
_transition_generation += 1
|
_transition_generation += 1
|
||||||
_cancel_presentation_tween()
|
_cancel_presentation_tween()
|
||||||
|
|
@ -921,6 +1387,7 @@ func close_menu(
|
||||||
if reason != CloseReason.USER:
|
if reason != CloseReason.USER:
|
||||||
_finish_close(reason, restore_controls, _menu_generation)
|
_finish_close(reason, restore_controls, _menu_generation)
|
||||||
return
|
return
|
||||||
|
_release_controller_ownership(false, true)
|
||||||
get_viewport().gui_cancel_drag()
|
get_viewport().gui_cancel_drag()
|
||||||
_close_sale_confirmation()
|
_close_sale_confirmation()
|
||||||
if reason in [
|
if reason in [
|
||||||
|
|
@ -949,6 +1416,7 @@ func close_for_session_end() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
|
_release_controller_ownership(false, true)
|
||||||
_cancel_presentation_tween()
|
_cancel_presentation_tween()
|
||||||
_cancel_page_tween()
|
_cancel_page_tween()
|
||||||
if visible:
|
if visible:
|
||||||
|
|
@ -1007,6 +1475,7 @@ func _show_section(section: Section) -> void:
|
||||||
and _profile_page.request_close_confirmation()
|
and _profile_page.request_close_confirmation()
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
_release_controller_ownership(false, true)
|
||||||
_begin_page_transition(section)
|
_begin_page_transition(section)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1122,22 +1591,39 @@ func _focus_current_section() -> void:
|
||||||
if _shop_cooler_context_active:
|
if _shop_cooler_context_active:
|
||||||
_focus_shop_cooler()
|
_focus_shop_cooler()
|
||||||
return
|
return
|
||||||
if _current_section == Section.COOLER:
|
_reserve_main_navigation_for_page_switching()
|
||||||
_cooler_sub_tab.grab_focus()
|
_reserve_visible_secondary_navigation()
|
||||||
elif _current_section == Section.BAG:
|
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
||||||
_bag_sub_tab.grab_focus()
|
var candidates: Array[Control] = []
|
||||||
elif _current_section == Section.TACKLE_BOX:
|
_collect_focusable_content(self, navigation_cluster, candidates)
|
||||||
_tackle_sub_tab.grab_focus()
|
if candidates.is_empty():
|
||||||
elif _current_section == Section.LOGBOOK:
|
return
|
||||||
_catalog_logbook.focus_initial()
|
candidates.sort_custom(func(first: Control, second: Control) -> bool:
|
||||||
elif _current_section == Section.NET:
|
if not is_equal_approx(first.global_position.y, second.global_position.y):
|
||||||
_the_net_page.focus_initial()
|
return first.global_position.y < second.global_position.y
|
||||||
elif _current_section == Section.MAIL:
|
return first.global_position.x < second.global_position.x
|
||||||
_mail_tab.grab_focus()
|
)
|
||||||
elif _current_section == Section.PLAYERS:
|
candidates[0].grab_focus()
|
||||||
_players_tab.grab_focus()
|
|
||||||
else:
|
|
||||||
_profile_tab.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:
|
func _process(delta: float) -> void:
|
||||||
|
|
@ -1161,6 +1647,7 @@ func _process(delta: float) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _configure_navigation_focus() -> void:
|
func _configure_navigation_focus() -> void:
|
||||||
|
_reserve_main_navigation_for_page_switching()
|
||||||
var navigation: Array[BubbleButtonType] = [
|
var navigation: Array[BubbleButtonType] = [
|
||||||
_inventory_tab,
|
_inventory_tab,
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
|
|
@ -1182,6 +1669,48 @@ func _configure_navigation_focus() -> void:
|
||||||
bubble.focus_neighbor_right = bubble.get_path_to(next)
|
bubble.focus_neighbor_right = bubble.get_path_to(next)
|
||||||
bubble.focus_neighbor_top = bubble.focus_neighbor_left
|
bubble.focus_neighbor_top = bubble.focus_neighbor_left
|
||||||
bubble.focus_neighbor_bottom = bubble.focus_neighbor_right
|
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] = [
|
var inventory_tabs: Array[Button] = [
|
||||||
_cooler_sub_tab,
|
_cooler_sub_tab,
|
||||||
_tackle_sub_tab,
|
_tackle_sub_tab,
|
||||||
|
|
@ -1359,6 +1888,7 @@ func _refresh_tackle_box() -> void:
|
||||||
row.text = "%s ×%d" % [item.display_name, owned.quantity]
|
row.text = "%s ×%d" % [item.display_name, owned.quantity]
|
||||||
row.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
row.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
row.toggle_mode = true
|
row.toggle_mode = true
|
||||||
|
row.set_meta(&"controller_tackle_item_id", owned.item_id)
|
||||||
row.button_pressed = owned.item_id == _selected_tackle_item_id
|
row.button_pressed = owned.item_id == _selected_tackle_item_id
|
||||||
if item.is_bait() and item.icon != null:
|
if item.is_bait() and item.icon != null:
|
||||||
_apply_tackle_bait_button_style(row)
|
_apply_tackle_bait_button_style(row)
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,32 @@ func _confirm(text: String, action: Callable) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _focus_first() -> void:
|
func _focus_first() -> void:
|
||||||
for child: Node in _tabs.get_children():
|
var candidates: Array[Control] = []
|
||||||
if child is Button and child.visible and not child.disabled:
|
_collect_focusable_player_controls(self, candidates)
|
||||||
child.grab_focus()
|
if candidates.is_empty():
|
||||||
return
|
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
|
class_name SettingsBubblePage
|
||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
|
const ControllerFocusNavigationType = preload(
|
||||||
|
"res://ui/controller_focus_navigation.gd"
|
||||||
|
)
|
||||||
|
|
||||||
@export var page_id: StringName
|
@export var page_id: StringName
|
||||||
@export var cluster_path: NodePath = ^"BubbleCluster"
|
@export var cluster_path: NodePath = ^"BubbleCluster"
|
||||||
@export var bubble_paths: Array[NodePath] = []
|
@export var bubble_paths: Array[NodePath] = []
|
||||||
|
|
@ -169,23 +173,13 @@ func _update_layout() -> void:
|
||||||
_cluster.position = _resting_cluster_position
|
_cluster.position = _resting_cluster_position
|
||||||
_cluster.size = field_size
|
_cluster.size = field_size
|
||||||
_cluster.apply_layout(field_size, compact)
|
_cluster.apply_layout(field_size, compact)
|
||||||
|
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_bubbles)
|
||||||
|
|
||||||
|
|
||||||
func _configure_focus_order() -> void:
|
func _configure_focus_order() -> void:
|
||||||
for bubble: BubbleButton in _bubbles:
|
for bubble: BubbleButton in _bubbles:
|
||||||
bubble.focus_mode = Control.FOCUS_NONE
|
bubble.focus_mode = Control.FOCUS_NONE
|
||||||
for index: int in _focus_bubbles.size():
|
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_bubbles)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
func _set_interactive(interactive: bool) -> void:
|
func _set_interactive(interactive: bool) -> void:
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ enum PresentationMode {
|
||||||
@onready var _mouse_value: BubbleButton = %MouseValue
|
@onready var _mouse_value: BubbleButton = %MouseValue
|
||||||
@onready var _controller_value: BubbleButton = %ControllerValue
|
@onready var _controller_value: BubbleButton = %ControllerValue
|
||||||
@onready var _invert_y_toggle: BubbleButton = %InvertYToggle
|
@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_toggle: BubbleButton = %AutoClickToggle
|
||||||
@onready var _auto_click_interval: BubbleButton = %AutoClickIntervalValue
|
@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 _mouse_sensitivity: float = 0.005
|
||||||
var _controller_sensitivity: float = 2.5
|
var _controller_sensitivity: float = 2.5
|
||||||
var _invert_camera_y: bool = false
|
var _invert_camera_y: bool = false
|
||||||
|
var _on_screen_keyboard_enabled: bool = false
|
||||||
var _network_profile: NetworkProfilePreferences
|
var _network_profile: NetworkProfilePreferences
|
||||||
var _network_session: NetworkSession
|
var _network_session: NetworkSession
|
||||||
var _data_root: PlayerDataRoot
|
var _data_root: PlayerDataRoot
|
||||||
|
|
@ -161,6 +163,7 @@ func _ready() -> void:
|
||||||
_adjust_controller_sensitivity.bind(1)
|
_adjust_controller_sensitivity.bind(1)
|
||||||
)
|
)
|
||||||
_invert_y_toggle.pressed.connect(_toggle_invert_y)
|
_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)
|
_auto_click_toggle.pressed.connect(_toggle_auto_click)
|
||||||
for control: Control in [_auto_click_toggle, _auto_click_interval]:
|
for control: Control in [_auto_click_toggle, _auto_click_interval]:
|
||||||
control.mouse_entered.connect(
|
control.mouse_entered.connect(
|
||||||
|
|
@ -704,6 +707,7 @@ func _apply_settings() -> void:
|
||||||
edited.mouse_camera_sensitivity = _mouse_sensitivity
|
edited.mouse_camera_sensitivity = _mouse_sensitivity
|
||||||
edited.controller_camera_sensitivity = _controller_sensitivity
|
edited.controller_camera_sensitivity = _controller_sensitivity
|
||||||
edited.invert_camera_y = _invert_camera_y
|
edited.invert_camera_y = _invert_camera_y
|
||||||
|
edited.on_screen_keyboard_enabled = _on_screen_keyboard_enabled
|
||||||
if _settings_manager.apply_settings(edited):
|
if _settings_manager.apply_settings(edited):
|
||||||
if (
|
if (
|
||||||
_presentation_mode == PresentationMode.TITLE_EMBEDDED
|
_presentation_mode == PresentationMode.TITLE_EMBEDDED
|
||||||
|
|
@ -725,6 +729,7 @@ func _load_controls() -> void:
|
||||||
_mouse_sensitivity = settings.mouse_camera_sensitivity
|
_mouse_sensitivity = settings.mouse_camera_sensitivity
|
||||||
_controller_sensitivity = settings.controller_camera_sensitivity
|
_controller_sensitivity = settings.controller_camera_sensitivity
|
||||||
_invert_camera_y = settings.invert_camera_y
|
_invert_camera_y = settings.invert_camera_y
|
||||||
|
_on_screen_keyboard_enabled = settings.on_screen_keyboard_enabled
|
||||||
_refresh_value_labels()
|
_refresh_value_labels()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -755,6 +760,10 @@ func _refresh_value_labels() -> void:
|
||||||
"invert\nvertical\ncamera\n"
|
"invert\nvertical\ncamera\n"
|
||||||
+ ("on" if _invert_camera_y else "off")
|
+ ("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 = (
|
_auto_click_toggle.text = (
|
||||||
"accessibility\nauto-click\n"
|
"accessibility\nauto-click\n"
|
||||||
+ ("on" if _auto_click_enabled else "off")
|
+ ("on" if _auto_click_enabled else "off")
|
||||||
|
|
@ -883,6 +892,11 @@ func _toggle_invert_y() -> void:
|
||||||
_refresh_value_labels()
|
_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:
|
func _toggle_auto_click() -> void:
|
||||||
_auto_click_enabled = not _auto_click_enabled
|
_auto_click_enabled = not _auto_click_enabled
|
||||||
_refresh_value_labels()
|
_refresh_value_labels()
|
||||||
|
|
|
||||||
|
|
@ -397,8 +397,8 @@ grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("3_page")
|
script = ExtResource("3_page")
|
||||||
page_id = &"controls"
|
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")])
|
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/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")
|
initial_focus_path = NodePath("BubbleCluster/MouseValue")
|
||||||
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
||||||
|
|
||||||
|
|
@ -491,17 +491,29 @@ neutral_size = Vector2(176, 170)
|
||||||
compact_minimum_size = Vector2(170, 164)
|
compact_minimum_size = Vector2(170, 164)
|
||||||
minimum_font_size = 14
|
minimum_font_size = 14
|
||||||
maximum_font_size = 24
|
maximum_font_size = 24
|
||||||
desktop_anchor = Vector2(525, 145)
|
desktop_anchor = Vector2(525, 105)
|
||||||
compact_anchor = Vector2(485, 135)
|
compact_anchor = Vector2(485, 95)
|
||||||
motion_phase = 4.5
|
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")]
|
[node name="ControllerMapping" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 0)
|
custom_minimum_size = Vector2(0, 0)
|
||||||
text = "map\ncontroller"
|
text = "map\ncontroller"
|
||||||
neutral_size = Vector2(160, 150)
|
neutral_size = Vector2(160, 150)
|
||||||
desktop_anchor = Vector2(525, 330)
|
desktop_anchor = Vector2(525, 390)
|
||||||
compact_anchor = Vector2(485, 300)
|
compact_anchor = Vector2(485, 355)
|
||||||
compact_minimum_size = Vector2(148, 138)
|
compact_minimum_size = Vector2(148, 138)
|
||||||
minimum_font_size = 14
|
minimum_font_size = 14
|
||||||
maximum_font_size = 23
|
maximum_font_size = 23
|
||||||
|
|
@ -512,8 +524,8 @@ unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 0)
|
custom_minimum_size = Vector2(0, 0)
|
||||||
text = "back"
|
text = "back"
|
||||||
neutral_size = Vector2(100, 94)
|
neutral_size = Vector2(100, 94)
|
||||||
desktop_anchor = Vector2(635, 445)
|
desktop_anchor = Vector2(635, 470)
|
||||||
compact_anchor = Vector2(555, 405)
|
compact_anchor = Vector2(555, 420)
|
||||||
compact_minimum_size = Vector2(84, 80)
|
compact_minimum_size = Vector2(84, 80)
|
||||||
minimum_font_size = 14
|
minimum_font_size = 14
|
||||||
maximum_font_size = 17
|
maximum_font_size = 17
|
||||||
|
|
|
||||||
|
|
@ -608,6 +608,10 @@ func _prepare_awaiting_start_input() -> void:
|
||||||
_navigation_focus_active = false
|
_navigation_focus_active = false
|
||||||
_modal_restore_navigation_focus = false
|
_modal_restore_navigation_focus = false
|
||||||
_button_center.show()
|
_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.show()
|
||||||
_feedback_label.modulate.a = 0.0
|
_feedback_label.modulate.a = 0.0
|
||||||
_continue_stats_hovered = false
|
_continue_stats_hovered = false
|
||||||
|
|
@ -650,6 +654,10 @@ func _reveal_primary_menu() -> void:
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
_awaiting_start_input = false
|
_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()
|
_stop_entry_prompt_animation()
|
||||||
_title_entry_generation += 1
|
_title_entry_generation += 1
|
||||||
_title_entry_transition_active = true
|
_title_entry_transition_active = true
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,13 @@ const MIN_UI_VIEWPORT_SIZE: Vector2i = Vector2i(256, 180)
|
||||||
const UIReferencePresentationType = preload(
|
const UIReferencePresentationType = preload(
|
||||||
"res://ui/ui_reference_presentation.gd"
|
"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(
|
signal effective_pixel_size_changed(
|
||||||
requested_pixel_size: int,
|
requested_pixel_size: int,
|
||||||
|
|
@ -16,6 +23,9 @@ signal effective_pixel_size_changed(
|
||||||
@onready var _canonical_stage: Control = (
|
@onready var _canonical_stage: Control = (
|
||||||
$UIViewport/GameUI/UIRoot/CanonicalStage
|
$UIViewport/GameUI/UIRoot/CanonicalStage
|
||||||
)
|
)
|
||||||
|
@onready var _hotbar: HotbarUI = (
|
||||||
|
$UIViewport/GameUI/UIRoot/CanonicalStage/Hotbar
|
||||||
|
)
|
||||||
@onready var _chat_ui: ChatUI = $UIViewport/GameUI/UIRoot/ChatUI
|
@onready var _chat_ui: ChatUI = $UIViewport/GameUI/UIRoot/ChatUI
|
||||||
@onready var _title_content_stage: Control = (
|
@onready var _title_content_stage: Control = (
|
||||||
$UIViewport/GameUI/UIRoot/TitleScreen/ResponsiveTitleStage
|
$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 _gameplay_active: bool = false
|
||||||
var _interactive_ui_open: bool = false
|
var _interactive_ui_open: bool = false
|
||||||
var _passive_pointer_ui_enabled: bool = false
|
var _passive_pointer_ui_enabled: bool = false
|
||||||
|
var _on_screen_keyboard: OnScreenKeyboardType
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
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()
|
var root_viewport: Viewport = get_viewport()
|
||||||
root_viewport.size_changed.connect(_resize_presentation)
|
root_viewport.size_changed.connect(_resize_presentation)
|
||||||
_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:
|
func set_pixel_size(pixel_size: int) -> void:
|
||||||
_requested_pixel_size = clampi(
|
_requested_pixel_size = clampi(
|
||||||
pixel_size,
|
pixel_size,
|
||||||
|
|
@ -120,6 +141,7 @@ func _resize_presentation() -> void:
|
||||||
UIReferencePresentationType.get_stage_position(display_size)
|
UIReferencePresentationType.get_stage_position(display_size)
|
||||||
)
|
)
|
||||||
_canonical_stage.size = UIReferencePresentationType.REFERENCE_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.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||||
_title_content_stage.position = _canonical_stage.position
|
_title_content_stage.position = _canonical_stage.position
|
||||||
_title_content_stage.size = UIReferencePresentationType.REFERENCE_SIZE
|
_title_content_stage.size = UIReferencePresentationType.REFERENCE_SIZE
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue