fix: stabilize controller menu navigation

This commit is contained in:
Alexander Sellite 2026-08-16 19:57:38 -04:00
parent 5df8b2e7cc
commit a8be597d27
32 changed files with 910 additions and 2376 deletions

View file

@ -1328,20 +1328,13 @@ func _handle_data_root_controller_input(event: InputEvent) -> bool:
if picker_visible if picker_visible
else _data_setup_dialog.gui_get_focus_owner() else _data_setup_dialog.gui_get_focus_owner()
) )
var focused_button := focused as BaseButton
if (
focused_button != null
and FileDialogControllerNavigation.activate_control(focused_button)
):
return true
if ( if (
(focused is LineEdit or focused is TextEdit) (focused is LineEdit or focused is TextEdit)
and _game_ui.request_controller_text_entry_for(focused) and _game_ui.request_controller_text_entry_for(focused)
): ):
return true return true
if setup_visible: # Buttons are activated once through Godot's native ui_accept path. This
_data_setup_dialog.get_ok_button().pressed.emit() # handler only owns modal back behavior and controller text entry.
return true
return false return false

View file

@ -13,7 +13,6 @@ readonly -a QUICK_TESTS=(
"tests/android_readiness_validation.gd" "tests/android_readiness_validation.gd"
"tests/camera_drag_validation.gd" "tests/camera_drag_validation.gd"
"tests/controller_focus_presentation_validation.gd" "tests/controller_focus_presentation_validation.gd"
"tests/controller_focus_recovery_validation.gd"
"tests/controller_menu_accessibility_validation.gd" "tests/controller_menu_accessibility_validation.gd"
"tests/controller_mapping_validation.gd" "tests/controller_mapping_validation.gd"
"tests/controller_world_interaction_validation.gd" "tests/controller_world_interaction_validation.gd"

View file

@ -1,184 +0,0 @@
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
original.grab_focus()
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"programmatic focus stays pending outside navigation mode",
)
var keyboard_navigation := InputEventKey.new()
keyboard_navigation.keycode = KEY_DOWN
keyboard_navigation.pressed = true
recovery._input(keyboard_navigation)
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == original,
"keyboard navigation restores pending initial focus",
)
var menu_shortcut := InputEventKey.new()
menu_shortcut.physical_keycode = KEY_TAB
menu_shortcut.pressed = true
recovery._input(menu_shortcut)
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"the menu shortcut does not leave a pseudo-focused option",
)
var controller_event := InputEventJoypadButton.new()
controller_event.button_index = JOY_BUTTON_A
controller_event.pressed = true
recovery._input(controller_event)
await process_frame
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == original,
"controller navigation restores pending initial 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 pointer_motion := InputEventMouseMotion.new()
pointer_motion.position = Vector2(4.0, 4.0)
recovery._input(pointer_motion)
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"mouse motion releases controller focus",
)
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"mouse motion prevents automatic focus recovery",
)
recovery._input(controller_event)
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == explicit_target,
"controller navigation can resume from pointer mode",
)
var pointer_press := InputEventMouseButton.new()
pointer_press.button_index = MOUSE_BUTTON_LEFT
pointer_press.pressed = true
recovery._input(pointer_press)
await process_frame
_expect(
root.gui_get_focus_owner() == explicit_target,
"mouse-down preserves focus through button activation",
)
var pointer_release := InputEventMouseButton.new()
pointer_release.button_index = MOUSE_BUTTON_LEFT
pointer_release.pressed = false
recovery._input(pointer_release)
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"mouse-up clears the completed button focus",
)
recovery._input(controller_event)
await process_frame
await process_frame
var outside_button := _make_button("outside", "outside")
stage.add_child(outside_button)
option_list.hide()
root.gui_release_focus()
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"focus recovery never escapes a hidden menu scope",
)
option_list.show()
explicit_target.grab_focus()
await process_frame
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",
)
var accessibility_only := Control.new()
accessibility_only.focus_mode = Control.FOCUS_ACCESSIBILITY
stage.add_child(accessibility_only)
await process_frame
_expect(
not bool(recovery.call("_is_focusable", accessibility_only)),
"accessibility-only controls are not recovery targets",
)
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)

View file

@ -44,6 +44,7 @@ func _run() -> void:
await _validate_join_game_navigation() await _validate_join_game_navigation()
await _validate_data_settings_navigation() await _validate_data_settings_navigation()
await _validate_settings_adjustment_navigation() await _validate_settings_adjustment_navigation()
await _validate_settings_presentation_requires_apply()
await _validate_mail_navigation() await _validate_mail_navigation()
await _validate_profile_confirmation_focus() await _validate_profile_confirmation_focus()
await _validate_profile_voice_navigation() await _validate_profile_voice_navigation()
@ -163,6 +164,47 @@ func _validate_join_game_navigation() -> void:
) )
await process_frame await process_frame
var saved_entries: Array[SavedServerEntry] = []
server_list.clear()
for index: int in 3:
var entry := SavedServerEntry.new()
entry.entry_id = "controller-saved-%d" % index
entry.display_name = "Saved server %d" % index
saved_entries.append(entry)
server_list.add_item(entry.display_name)
page.set("_mode", 2)
page.set("_visible_entries", saved_entries)
server_list.show()
page.call("_configure_controller_navigation")
page.call("_restore_entry_selection_and_focus", 1)
await process_frame
_expect(
server_list.is_selected(1)
and root.gui_get_focus_owner() == server_list,
"Saved-server refresh did not restore the same list position.",
)
saved_entries.remove_at(1)
server_list.remove_item(1)
page.set("_visible_entries", saved_entries)
page.call("_restore_entry_selection_and_focus", 1)
await process_frame
_expect(
server_list.is_selected(1)
and (
page.get("_selected_entry") as SavedServerEntry
).entry_id == "controller-saved-2",
"Deleting a saved server did not select the row that replaced it.",
)
saved_entries.clear()
server_list.clear()
page.set("_visible_entries", saved_entries)
page.call("_restore_entry_selection_and_focus", 0)
await process_frame
_expect(
root.gui_get_focus_owner() == saved,
"An empty saved-server list did not return focus to its mode tab.",
)
page.queue_free() page.queue_free()
await process_frame await process_frame
@ -190,13 +232,13 @@ func _validate_data_settings_navigation() -> void:
open_activation["count"] = int(open_activation["count"]) + 1 open_activation["count"] = int(open_activation["count"]) + 1
) )
data_tab.grab_focus() data_tab.grab_focus()
var accept_press := InputEventAction.new() var accept_press := InputEventJoypadButton.new()
accept_press.action = &"ui_accept" accept_press.button_index = JOY_BUTTON_A
accept_press.pressed = true accept_press.pressed = true
Input.parse_input_event(accept_press) Input.parse_input_event(accept_press)
await process_frame await process_frame
var accept_release := InputEventAction.new() var accept_release := InputEventJoypadButton.new()
accept_release.action = &"ui_accept" accept_release.button_index = JOY_BUTTON_A
accept_release.pressed = false accept_release.pressed = false
Input.parse_input_event(accept_release) Input.parse_input_event(accept_release)
for _frame: int in 2: for _frame: int in 2:
@ -213,10 +255,6 @@ func _validate_data_settings_navigation() -> void:
root.gui_get_focus_owner() == data_tab, root.gui_get_focus_owner() == data_tab,
"Selecting the Data tab transferred focus into its actions.", "Selecting the Data tab transferred focus into its actions.",
) )
var test_data_root := PlayerDataRoot.new()
root.add_child(test_data_root)
test_data_root.root_path = "/tmp/netfishing-controller-data"
panel.set("_data_root", test_data_root)
open_data_folder.grab_focus() open_data_folder.grab_focus()
_expect( _expect(
root.gui_get_focus_owner() == open_data_folder, root.gui_get_focus_owner() == open_data_folder,
@ -226,13 +264,13 @@ func _validate_data_settings_navigation() -> void:
not open_data_folder.disabled, not open_data_folder.disabled,
"Open Data Folder is unexpectedly disabled.", "Open Data Folder is unexpectedly disabled.",
) )
var action_press := InputEventAction.new() var action_press := InputEventJoypadButton.new()
action_press.action = &"ui_accept" action_press.button_index = JOY_BUTTON_A
action_press.pressed = true action_press.pressed = true
Input.parse_input_event(action_press) Input.parse_input_event(action_press)
await process_frame await process_frame
var action_release := InputEventAction.new() var action_release := InputEventJoypadButton.new()
action_release.action = &"ui_accept" action_release.button_index = JOY_BUTTON_A
action_release.pressed = false action_release.pressed = false
Input.parse_input_event(action_release) Input.parse_input_event(action_release)
for _frame: int in 2: for _frame: int in 2:
@ -244,31 +282,27 @@ func _validate_data_settings_navigation() -> void:
% int(open_activation["count"]) % int(open_activation["count"])
), ),
) )
var confirmations: Array[Node] = panel.find_children(
"*", "ConfirmationDialog", true, false
)
_expect( _expect(
confirmations.size() == 1 panel.find_children(
and (confirmations.front() as ConfirmationDialog).visible, "*", "ConfirmationDialog", true, false
"Explicitly accepting Open Data Folder did not request confirmation.", ).is_empty(),
"Open Data Folder left a controller-activatable dialog behind.",
) )
if not confirmations.is_empty(): var change_data_folder := panel.get_node("%ChangeDataFolder") as Button
var confirmation := confirmations.front() as ConfirmationDialog var copy_fingerprint := panel.get_node("%CopyPlayerFingerprint") as Button
_expect( var export_player := panel.get_node("%ExportPlayerIdentity") as Button
confirmation.gui_get_focus_owner() var import_player := panel.get_node("%ImportPlayerIdentity") as Button
== confirmation.get_cancel_button(), var export_host := panel.get_node("%ExportHostIdentity") as Button
"Open Data Folder confirmation did not default to Cancel.", var import_host := panel.get_node("%ImportHostIdentity") as Button
)
confirmation.queue_free()
var controls: Array[Control] = [ var controls: Array[Control] = [
data_tab, data_tab,
open_data_folder, open_data_folder,
panel.get_node("%ChangeDataFolder") as Control, change_data_folder,
panel.get_node("%CopyPlayerFingerprint") as Control, copy_fingerprint,
panel.get_node("%ExportPlayerIdentity") as Control, export_player,
panel.get_node("%ImportPlayerIdentity") as Control, import_player,
panel.get_node("%ExportHostIdentity") as Control, export_host,
panel.get_node("%ImportHostIdentity") as Control, import_host,
panel.get_node("%ApplySettingsButton") as Control, panel.get_node("%ApplySettingsButton") as Control,
panel.get_node("%SettingsBackButton") as Control, panel.get_node("%SettingsBackButton") as Control,
] ]
@ -278,13 +312,113 @@ func _validate_data_settings_navigation() -> void:
"Data & Identity control %s is not controller-focusable." "Data & Identity control %s is not controller-focusable."
% control.name, % control.name,
) )
_assert_neighbor(data_tab, &"focus_neighbor_bottom", open_data_folder)
_assert_neighbor(
open_data_folder,
&"focus_neighbor_right",
change_data_folder,
)
_assert_neighbor(
open_data_folder,
&"focus_neighbor_bottom",
copy_fingerprint,
)
_assert_neighbor(
change_data_folder,
&"focus_neighbor_left",
open_data_folder,
)
_assert_neighbor(
change_data_folder,
&"focus_neighbor_bottom",
copy_fingerprint,
)
_assert_neighbor(
copy_fingerprint,
&"focus_neighbor_top",
open_data_folder,
)
_assert_neighbor(
copy_fingerprint,
&"focus_neighbor_bottom",
export_player,
)
_assert_neighbor(export_player, &"focus_neighbor_right", import_player)
_assert_neighbor(export_player, &"focus_neighbor_bottom", export_host)
_assert_neighbor(import_player, &"focus_neighbor_left", export_player)
_assert_neighbor(import_player, &"focus_neighbor_bottom", import_host)
_assert_neighbor(export_host, &"focus_neighbor_top", export_player)
_assert_neighbor(export_host, &"focus_neighbor_right", import_host)
_assert_neighbor(import_host, &"focus_neighbor_top", import_player)
_assert_neighbor(import_host, &"focus_neighbor_left", export_host)
_assert_directionally_reachable(controls.front(), controls) _assert_directionally_reachable(controls.front(), controls)
await process_frame
open_data_folder.grab_focus()
panel.call("_select_page", &"sound", true)
_expect(
panel.get_active_page_id() == &"sound",
"Settings did not leave the Data page.",
)
_expect(
root.gui_get_focus_owner() == panel.get_node("%SoundTab"),
"Leaving Data retained focus on a hidden Data action.",
)
var sound_down: Control = (
(panel.get_node("%SoundTab") as Control).find_valid_focus_neighbor(
SIDE_BOTTOM
)
)
_expect(
sound_down != null and not panel.get_node("%DataPage").is_ancestor_of(
sound_down
),
"Sound navigation still pointed into the hidden Data page.",
)
var hidden_data_activation_count: int = int(open_activation["count"])
Input.parse_input_event(action_press)
await process_frame
Input.parse_input_event(action_release)
for _frame: int in 2:
await process_frame
_expect(
int(open_activation["count"]) == hidden_data_activation_count,
"Controller A activated Open Data Folder from another settings page.",
)
open_data_folder.pressed.emit()
await process_frame
_expect(
panel.find_children(
"*", "ConfirmationDialog", true, false
).is_empty(),
"A hidden Data action created a folder dialog.",
)
panel.call("_select_page", &"data", true)
await process_frame
copy_fingerprint.grab_focus()
var feedback := panel.get_node("%SettingsFeedback") as Label
feedback.text = "activation boundary intact"
open_data_folder.pressed.emit()
await process_frame
_expect(
feedback.text == "activation boundary intact",
(
"Open Data Folder crossed its activation boundary without owning "
+ "controller focus."
),
)
panel.hide()
await process_frame
var hidden_focus_owner: Control = root.gui_get_focus_owner()
_expect(
hidden_focus_owner == null
or not panel.is_ancestor_of(hidden_focus_owner),
"Hiding Settings retained focus on one of its controls.",
)
_expect( _expect(
panel.find_children("*", "BubbleButton", true, false).is_empty(), panel.find_children("*", "BubbleButton", true, false).is_empty(),
"Settings children still contain bubble controls.", "Settings children still contain bubble controls.",
) )
panel.queue_free() panel.queue_free()
test_data_root.queue_free()
await process_frame await process_frame
@ -313,6 +447,11 @@ func _validate_settings_adjustment_navigation() -> void:
var controller_slider := panel.get_node( var controller_slider := panel.get_node(
"%ControllerSensitivitySlider" "%ControllerSensitivitySlider"
) as HSlider ) as HSlider
var on_screen_keyboard := panel.get_node(
"%OnScreenKeyboardToggle"
) as Button
var controller_binds := panel.get_node("%ControllerMapping") as Button
var keyboard_binds := panel.get_node("%KeyboardMapping") as Button
_expect( _expect(
mouse_slider.focus_mode == Control.FOCUS_ALL mouse_slider.focus_mode == Control.FOCUS_ALL
and controller_slider.focus_mode == Control.FOCUS_ALL, and controller_slider.focus_mode == Control.FOCUS_ALL,
@ -323,14 +462,39 @@ func _validate_settings_adjustment_navigation() -> void:
and is_equal_approx(controller_slider.step, 0.1), and is_equal_approx(controller_slider.step, 0.1),
"Sensitivity sliders do not retain their authored increments.", "Sensitivity sliders do not retain their authored increments.",
) )
_assert_neighbor(
on_screen_keyboard,
&"focus_neighbor_bottom",
controller_binds,
)
_assert_neighbor(
controller_binds,
&"focus_neighbor_top",
on_screen_keyboard,
)
_assert_neighbor(
controller_binds,
&"focus_neighbor_right",
keyboard_binds,
)
_assert_neighbor(
keyboard_binds,
&"focus_neighbor_top",
on_screen_keyboard,
)
_assert_neighbor(
keyboard_binds,
&"focus_neighbor_left",
controller_binds,
)
var control_inputs: Array[Control] = [ var control_inputs: Array[Control] = [
panel.get_node("%ControlsTab") as Control, panel.get_node("%ControlsTab") as Control,
mouse_slider, mouse_slider,
controller_slider, controller_slider,
panel.get_node("%InvertYToggle") as Control, panel.get_node("%InvertYToggle") as Control,
panel.get_node("%OnScreenKeyboardToggle") as Control, on_screen_keyboard,
panel.get_node("%ControllerMapping") as Control, controller_binds,
panel.get_node("%KeyboardMapping") as Control, keyboard_binds,
panel.get_node("%ApplySettingsButton") as Control, panel.get_node("%ApplySettingsButton") as Control,
panel.get_node("%SettingsBackButton") as Control, panel.get_node("%SettingsBackButton") as Control,
] ]
@ -339,6 +503,38 @@ func _validate_settings_adjustment_navigation() -> void:
await process_frame await process_frame
func _validate_settings_presentation_requires_apply() -> void:
var panel := SettingsPanelScene.instantiate() as SettingsPanel
var settings_manager := PlayerSettingsManager.new()
root.add_child(settings_manager)
root.add_child(panel)
await process_frame
panel.open_panel(settings_manager)
await process_frame
var chat_dock := panel.get_node("%ChatDockSelector") as OptionButton
var chat_mode := panel.get_node("%ChatModeSelector") as OptionButton
var paint_dock := panel.get_node("%PaintDockSelector") as OptionButton
chat_dock.select(1)
chat_dock.item_selected.emit(1)
chat_mode.select(1)
chat_mode.item_selected.emit(1)
paint_dock.select(0)
paint_dock.item_selected.emit(0)
_expect(
not settings_manager.current_settings.chat_dock_right
and not settings_manager.current_settings.chat_mobile_mode
and settings_manager.current_settings.paint_dock_right,
(
"Navigating presentation choices changed saved docking settings "
+ "before Apply."
),
)
panel.close_panel(true)
panel.queue_free()
settings_manager.queue_free()
await process_frame
func _validate_mail_navigation() -> void: func _validate_mail_navigation() -> void:
var page := MailPageType.new() as Control var page := MailPageType.new() as Control
root.add_child(page) root.add_child(page)
@ -1334,13 +1530,8 @@ func _assert_directionally_reachable(
var pending: Array[Control] = [start] var pending: Array[Control] = [start]
while not pending.is_empty(): while not pending.is_empty():
var current: Control = pending.pop_front() var current: Control = pending.pop_front()
for path: NodePath in [ for side: Side in [SIDE_LEFT, SIDE_RIGHT, SIDE_TOP, SIDE_BOTTOM]:
current.focus_neighbor_left, var neighbor: Control = current.find_valid_focus_neighbor(side)
current.focus_neighbor_right,
current.focus_neighbor_top,
current.focus_neighbor_bottom,
]:
var neighbor := current.get_node_or_null(path) as Control
if ( if (
neighbor == null neighbor == null
or not expected.has(neighbor.get_instance_id()) or not expected.has(neighbor.get_instance_id())

View file

@ -151,7 +151,11 @@ func _validate_player_page_zone_contracts() -> void:
var net_source: String = FileAccess.get_file_as_string( var net_source: String = FileAccess.get_file_as_string(
"res://ui/the_net_page.gd" "res://ui/the_net_page.gd"
) )
assert(net_source.contains("ROLE_RIGHT_STICK_Y")) var game_ui_source: String = FileAccess.get_file_as_string(
"res://ui/game_ui.gd"
)
assert(not net_source.contains("ROLE_RIGHT_STICK_Y"))
assert(game_ui_source.contains("_update_controller_menu_scroll"))
func _validate_shop_navigation_contract() -> void: func _validate_shop_navigation_contract() -> void:

View file

@ -172,7 +172,15 @@ func _run() -> void:
) )
) )
assert(dialog.gui_get_focus_owner() == select_button) assert(dialog.gui_get_focus_owner() == select_button)
assert(FileDialogControllerNavigationType.activate_control(sort_button)) sort_button.grab_focus()
var accept_press := InputEventAction.new()
accept_press.action = &"ui_accept"
accept_press.pressed = true
Input.parse_input_event(accept_press)
var accept_release := InputEventAction.new()
accept_release.action = &"ui_accept"
accept_release.pressed = false
Input.parse_input_event(accept_release)
await process_frame await process_frame
assert(sort_button.get_popup().visible) assert(sort_button.get_popup().visible)
sort_button.get_popup().hide() sort_button.get_popup().hide()
@ -283,13 +291,16 @@ func _validate_compact_dialog() -> void:
var down_event := InputEventJoypadButton.new() var down_event := InputEventJoypadButton.new()
down_event.button_index = JOY_BUTTON_DPAD_DOWN down_event.button_index = JOY_BUTTON_DPAD_DOWN
down_event.pressed = true down_event.pressed = true
font_controller.call("_input", down_event) var dialog_controller := font_controller.get(
"_file_dialog_controller"
) as FileDialogController
dialog_controller.call("_input", down_event)
assert(scope.gui_get_focus_owner() == select_button) assert(scope.gui_get_focus_owner() == select_button)
path_edit.grab_focus() path_edit.grab_focus()
var accept_event := InputEventJoypadButton.new() var accept_event := InputEventJoypadButton.new()
accept_event.button_index = JOY_BUTTON_A accept_event.button_index = JOY_BUTTON_A
accept_event.pressed = true accept_event.pressed = true
font_controller.call("_input", accept_event) dialog_controller.call("_input", accept_event)
assert(keyboard.is_open()) assert(keyboard.is_open())
keyboard.call("_close_keyboard", true) keyboard.call("_close_keyboard", true)
dialog.queue_free() dialog.queue_free()

View file

@ -32,6 +32,9 @@ func _run() -> void:
var accept := InputEventJoypadButton.new() var accept := InputEventJoypadButton.new()
accept.button_index = JOY_BUTTON_A accept.button_index = JOY_BUTTON_A
accept.pressed = true accept.pressed = true
var accept_release := InputEventJoypadButton.new()
accept_release.button_index = JOY_BUTTON_A
accept_release.pressed = false
var cancel := InputEventJoypadButton.new() var cancel := InputEventJoypadButton.new()
cancel.button_index = JOY_BUTTON_B cancel.button_index = JOY_BUTTON_B
cancel.pressed = true cancel.pressed = true
@ -72,7 +75,10 @@ func _run() -> void:
)) ))
if section_index == FishingShop.ShopSection.BAIT: if section_index == FishingShop.ShopSection.BAIT:
assert(root.gui_get_focus_owner() == dummy_stock) assert(root.gui_get_focus_owner() == dummy_stock)
shop.call("_input", accept) Input.parse_input_event(accept)
await process_frame
Input.parse_input_event(accept_release)
await process_frame
assert(activations[0] == 1) assert(activations[0] == 1)
shop.call("_input", cancel) shop.call("_input", cancel)
for _frame: int in 2: for _frame: int in 2:

View file

@ -23,9 +23,6 @@ enum InitialFocus {
^"BubbleCluster/CancelButton/CancelLabel" ^"BubbleCluster/CancelButton/CancelLabel"
) )
@export var maximum_layout_size: Vector2 = Vector2(720.0, 520.0) @export var maximum_layout_size: Vector2 = Vector2(720.0, 520.0)
@export var compact_maximum_layout_size: Vector2 = Vector2(544.0, 400.0)
@export var compact_width_threshold: float = 680.0
@export var compact_height_threshold: float = 500.0
@export_range(0.0, 128.0, 1.0) var transition_safe_margin: float = 24.0 @export_range(0.0, 128.0, 1.0) var transition_safe_margin: float = 24.0
var _cluster: BubbleCluster var _cluster: BubbleCluster
@ -158,15 +155,9 @@ func _process(delta: float) -> void:
func _update_layout() -> void: func _update_layout() -> void:
if not is_node_ready() or _cluster == null: if not is_node_ready() or _cluster == null:
return return
var compact: bool = false
var layout_maximum: Vector2 = (
compact_maximum_layout_size
if compact
else maximum_layout_size
)
var field_size := Vector2( var field_size := Vector2(
minf(size.x, layout_maximum.x), minf(size.x, maximum_layout_size.x),
minf(size.y, layout_maximum.y) minf(size.y, maximum_layout_size.y)
) )
field_size.x = maxf(1.0, field_size.x) field_size.x = maxf(1.0, field_size.x)
field_size.y = maxf(1.0, field_size.y) field_size.y = maxf(1.0, field_size.y)
@ -174,7 +165,7 @@ func _update_layout() -> void:
if not _is_transitioning: if not _is_transitioning:
_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, false)
func _configure_focus() -> void: func _configure_focus() -> void:

View file

@ -1,277 +0,0 @@
class_name ControllerFocusRecovery
extends Node
const CONTROLLER_AXIS_THRESHOLD: float = 0.35
const SEMANTIC_MATCH_BONUS: float = 1000000.0
const KEYBOARD_NAVIGATION_ACTIONS: Array[StringName] = [
&"ui_accept",
&"ui_cancel",
&"ui_up",
&"ui_down",
&"ui_left",
&"ui_right",
&"ui_focus_next",
&"ui_focus_prev",
&"ui_page_up",
&"ui_page_down",
&"ui_home",
&"ui_end",
]
var _controller_active: bool = false
var _focus_navigation_active: bool = false
var _last_focus_center: Vector2 = Vector2.ZERO
var _last_focus_key: String = ""
var _scope_chain: Array[WeakRef] = []
var _pending_focus: WeakRef
var _pointer_button_down: bool = false
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:
_pointer_button_down = false
_controller_active = true
_focus_navigation_active = true
if button_event.button_index == JOY_BUTTON_LEFT_SHOULDER:
_recovery_generation += 1
_scope_chain.clear()
_pending_focus = null
return
_request_pending_focus()
return
if event is InputEventJoypadMotion:
if absf((event as InputEventJoypadMotion).axis_value) >= (
CONTROLLER_AXIS_THRESHOLD
):
_pointer_button_down = false
_controller_active = true
_focus_navigation_active = true
_request_pending_focus()
return
if event is InputEventMouseMotion:
_pointer_button_down = (
(event as InputEventMouseMotion).button_mask != 0
)
_leave_focus_navigation(not _pointer_button_down)
return
if event is InputEventMouseButton:
var mouse_button := event as InputEventMouseButton
if mouse_button.button_index in [
MOUSE_BUTTON_LEFT,
MOUSE_BUTTON_RIGHT,
MOUSE_BUTTON_MIDDLE,
]:
_pointer_button_down = mouse_button.pressed
_leave_focus_navigation(false)
if not mouse_button.pressed:
_release_current_pointer_focus.call_deferred()
return
if event is InputEventKey and (event as InputEventKey).pressed:
_pointer_button_down = false
_controller_active = false
if _is_keyboard_navigation_event(event):
_focus_navigation_active = true
_request_pending_focus()
else:
_leave_focus_navigation()
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 _focus_navigation_active:
_pending_focus = null
if _controller_active and control is BaseButton:
_remember_focus(control)
return
if _keeps_pointer_focus(control):
return
if _is_focusable(control):
_pending_focus = weakref(control)
_release_focus_if_inactive.call_deferred(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()
):
continue
if not scope.is_visible_in_tree():
_scope_chain.clear()
return
var replacement := _best_replacement_in(scope)
if replacement != null:
replacement.grab_focus()
else:
_scope_chain.clear()
return
_scope_chain.clear()
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 not in [Control.FOCUS_CLICK, Control.FOCUS_ALL]
):
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]
func _leave_focus_navigation(release_focus: bool = true) -> void:
_controller_active = false
_focus_navigation_active = false
_recovery_generation += 1
_scope_chain.clear()
var focus_owner: Control = get_viewport().gui_get_focus_owner()
if focus_owner == null or _keeps_pointer_focus(focus_owner):
return
if _is_focusable(focus_owner):
_pending_focus = weakref(focus_owner)
if release_focus:
_release_focus_if_inactive.call_deferred(focus_owner)
func _release_focus_if_inactive(control: Control) -> void:
if (
_focus_navigation_active
or _pointer_button_down
or control == null
or not is_instance_valid(control)
or get_viewport().gui_get_focus_owner() != control
or _keeps_pointer_focus(control)
):
return
get_viewport().gui_release_focus()
func _release_current_pointer_focus() -> void:
var focus_owner: Control = get_viewport().gui_get_focus_owner()
if focus_owner != null:
_release_focus_if_inactive(focus_owner)
func _request_pending_focus() -> void:
if (
not _focus_navigation_active
or _pending_focus == null
or get_viewport().gui_get_focus_owner() != null
):
return
_restore_pending_focus.call_deferred(_recovery_generation)
func _restore_pending_focus(generation: int) -> void:
if (
generation != _recovery_generation
or not _focus_navigation_active
or _pending_focus == null
or get_viewport().gui_get_focus_owner() != null
):
return
var target := _pending_focus.get_ref() as Control
if not _is_focusable(target):
_pending_focus = null
return
_pending_focus = null
target.grab_focus()
func _is_keyboard_navigation_event(event: InputEvent) -> bool:
if event.is_action_pressed(&"open_backpack"):
return false
if (
event.is_action_pressed(&"ui_cancel")
and get_viewport().gui_get_focus_owner() == null
):
return false
for action: StringName in KEYBOARD_NAVIGATION_ACTIONS:
if event.is_action_pressed(action):
return true
return false
func _keeps_pointer_focus(control: Control) -> bool:
return control is LineEdit or control is TextEdit

View file

@ -1,153 +1,28 @@
class_name EmoteRadialMenu class_name EmoteRadialMenu
extends Control extends RadialActionMenu
signal emote_selected(emote_id: StringName) signal emote_selected(emote_id: StringName)
const BubbleButtonScene: PackedScene = preload(
"res://ui/components/bubble_menu/bubble_button.tscn"
)
const BubbleProfile: BubbleMenuProfile = preload(
"res://ui/components/bubble_menu/bubble_menu_profile.tres"
)
const SECTOR_COUNT: int = 8 const SECTOR_COUNT: int = 8
const SIT_SECTOR: int = 0 const SIT_SECTOR: int = 0
const RING_RADIUS: float = 172.0
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
var _buttons: Array[BubbleButton] = []
var _is_open: bool = false
var _selected_sector: int = SIT_SECTOR
var _controller_selection_mode: bool = false
var _controller_mapping_manager: ControllerMappingManagerType
func setup_controller_mapping( func _input_action_name() -> StringName:
mapping_manager: ControllerMappingManagerType, return &"open_emotes"
) -> void:
_controller_mapping_manager = mapping_manager
func _ready() -> void: func _sector_count() -> int:
visible = false return SECTOR_COUNT
mouse_filter = Control.MOUSE_FILTER_IGNORE
for sector: int in SECTOR_COUNT:
var bubble: BubbleButton = BubbleButtonScene.instantiate() as BubbleButton
bubble.profile = BubbleProfile
bubble.focus_mode = Control.FOCUS_NONE
bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
bubble.text = "Sit" if sector == SIT_SECTOR else ""
add_child(bubble)
_buttons.append(bubble)
_apply_selection_styles()
func handle_input(event: InputEvent, can_open: bool) -> bool: func _initial_sector() -> int:
if not event.is_action("open_emotes"): return SIT_SECTOR
return false
var key_event: InputEventKey = event as InputEventKey
if key_event != null and key_event.echo: func _label_for_sector(sector: int) -> String:
return false return "Sit" if sector == SIT_SECTOR else ""
if event.is_pressed():
if _is_open or not can_open:
return _is_open func _emit_selected_sector(sector: int) -> void:
open_menu(event is InputEventJoypadButton) if sector == SIT_SECTOR:
return true
if not _is_open:
return false
var selected: int = _selected_sector
close_menu()
if selected == SIT_SECTOR:
emote_selected.emit(&"sit") emote_selected.emit(&"sit")
return true
func open_menu(controller_selection: bool = false) -> void:
_is_open = true
_selected_sector = SIT_SECTOR
_controller_selection_mode = controller_selection
visible = true
_layout_bubbles()
_apply_selection_styles()
func close_menu() -> void:
_is_open = false
visible = false
func is_open() -> bool:
return _is_open
func _process(_delta: float) -> void:
if not _is_open:
return
_layout_bubbles()
_update_selection()
func _layout_bubbles() -> void:
var center: Vector2 = size * 0.5
for sector: int in SECTOR_COUNT:
var angle: float = -PI * 0.5 + TAU * float(sector) / float(SECTOR_COUNT)
var bubble_center: Vector2 = center + Vector2.from_angle(angle) * RING_RADIUS
var bubble: BubbleButton = _buttons[sector]
bubble.position = bubble_center - BUBBLE_SIZE * 0.5
bubble.size = BUBBLE_SIZE
bubble.pivot_offset = BUBBLE_SIZE * 0.5
func _update_selection() -> void:
var stick: Vector2 = _get_selection_stick()
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
_select_sector_from_offset(stick)
return
if _controller_selection_mode:
return
_update_mouse_selection()
func _get_selection_stick() -> Vector2:
if _controller_mapping_manager != null:
return Vector2(
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
),
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
),
)
return Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
)
func _update_mouse_selection() -> void:
var offset: Vector2 = get_local_mouse_position() - size * 0.5
if offset.length() < 24.0:
return
_select_sector_from_offset(offset)
func _select_sector_from_offset(offset: Vector2) -> void:
var angle: float = fposmod(offset.angle() + PI * 0.5 + PI / 8.0, TAU)
var sector: int = int(floor(angle / (TAU / float(SECTOR_COUNT))))
if sector == _selected_sector:
return
_selected_sector = sector
_apply_selection_styles()
func _apply_selection_styles() -> void:
for sector: int in _buttons.size():
var bubble: BubbleButton = _buttons[sector]
bubble.apply_profile()
if sector == _selected_sector:
bubble.add_theme_stylebox_override(
"normal", BubbleProfile.make_hover_style()
)

View file

@ -0,0 +1,128 @@
class_name FileDialogController
extends Node
const FileDialogControllerNavigationType = preload(
"res://ui/file_dialog_controller_navigation.gd"
)
var _text_entry_request: Callable
var _text_entry_is_open: Callable
var _tracked_dialogs: Dictionary[int, WeakRef] = {}
func setup_text_entry(
request: Callable,
is_open: Callable = Callable(),
) -> void:
_text_entry_request = request
_text_entry_is_open = is_open
func track_dialog(dialog: FileDialog) -> void:
if dialog != null:
_tracked_dialogs[dialog.get_instance_id()] = weakref(dialog)
func _process(_delta: float) -> void:
if _text_entry_is_active():
return
for instance_id: int in _tracked_dialogs.keys():
var reference: WeakRef = _tracked_dialogs[instance_id]
var dialog := reference.get_ref() as FileDialog
if dialog == null or not is_instance_valid(dialog):
_tracked_dialogs.erase(instance_id)
continue
if not dialog.visible:
continue
FileDialogControllerNavigationType.configure(dialog)
_connect_text_controls(dialog)
func _input(event: InputEvent) -> void:
if _text_entry_is_active():
return
if not (event is InputEventJoypadButton or event is InputEventJoypadMotion):
return
var scope: Window = _active_scope()
if scope == null:
return
var focused: Control = scope.gui_get_focus_owner()
if focused == null:
return
var direction: Vector2 = _controller_direction(event)
var item_list := focused as ItemList
if (
item_list != null
and direction != Vector2.ZERO
and FileDialogControllerNavigationType.move_from_item_list(
item_list, direction
)
):
get_viewport().set_input_as_handled()
return
var button_event := event as InputEventJoypadButton
if (
button_event != null
and button_event.pressed
and event.is_action_pressed(&"ui_accept")
and (focused is LineEdit or focused is TextEdit)
and _text_entry_request.is_valid()
and bool(_text_entry_request.call(focused))
):
get_viewport().set_input_as_handled()
func _active_scope() -> Window:
for reference_value: WeakRef in _tracked_dialogs.values():
var dialog := reference_value.get_ref() as FileDialog
if dialog == null or not is_instance_valid(dialog) or not dialog.visible:
continue
return FileDialogControllerNavigationType.active_scope(dialog)
return null
func _controller_direction(event: InputEvent) -> Vector2:
if event.is_action_pressed(&"ui_up"):
return Vector2.UP
if event.is_action_pressed(&"ui_down"):
return Vector2.DOWN
if event.is_action_pressed(&"ui_left"):
return Vector2.LEFT
if event.is_action_pressed(&"ui_right"):
return Vector2.RIGHT
return Vector2.ZERO
func _connect_text_controls(dialog: FileDialog) -> void:
var scope: Window = FileDialogControllerNavigationType.active_scope(dialog)
for control: Control in (
FileDialogControllerNavigationType.interactive_controls(scope)
):
if not (control is LineEdit or control is TextEdit):
continue
var callback := _on_text_gui_input.bind(control)
if not control.gui_input.is_connected(callback):
control.gui_input.connect(callback)
func _on_text_gui_input(event: InputEvent, control: Control) -> void:
var button_event := event as InputEventJoypadButton
if (
button_event == null
or not button_event.pressed
or (
button_event.button_index != JOY_BUTTON_A
and not event.is_action_pressed(&"ui_accept")
)
or not _text_entry_request.is_valid()
):
return
if bool(_text_entry_request.call(control)):
control.accept_event()
func _text_entry_is_active() -> bool:
return (
_text_entry_is_open.is_valid()
and bool(_text_entry_is_open.call())
)

View file

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

View file

@ -72,20 +72,6 @@ static func interactive_controls(scope: Window) -> Array[Control]:
return controls return controls
static func activate_control(control: Control) -> bool:
if control == null or not control.is_visible_in_tree():
return false
var menu_button := control as MenuButton
if menu_button != null and not menu_button.disabled:
menu_button.show_popup()
return true
var button := control as BaseButton
if button != null and not button.disabled:
button.pressed.emit()
return true
return false
static func move_from_item_list( static func move_from_item_list(
item_list: ItemList, item_list: ItemList,
direction: Vector2, direction: Vector2,

View file

@ -236,13 +236,15 @@ func _input(event: InputEvent) -> void:
close_shop() close_shop()
return return
if uses_accept: if uses_accept:
if _controller_zone != ControllerZone.TABS:
# Content buttons use Godot's native ui_accept activation. Keeping a
# second manual pressed.emit() path here allowed one controller press
# to be interpreted by two different owners.
return
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
if not button_event.pressed or _transaction_in_progress: if not button_event.pressed or _transaction_in_progress:
return return
if _controller_zone == ControllerZone.TABS: _enter_shop_content_zone()
_enter_shop_content_zone()
else:
_activate_focused_shop_control()
return return
var uses_left_bumper: bool = ( var uses_left_bumper: bool = (
_controller_mapping_manager.event_uses_role( _controller_mapping_manager.event_uses_role(
@ -295,19 +297,6 @@ func _enter_shop_content_zone() -> void:
_select_shop_section(section_index, true) _select_shop_section(section_index, true)
func _activate_focused_shop_control() -> bool:
var focused := get_viewport().gui_get_focus_owner() as BaseButton
if (
focused == null
or focused.disabled
or _is_shop_tab(focused)
or not focused.is_visible_in_tree()
):
return false
focused.pressed.emit()
return true
func _focused_shop_tab_index() -> int: func _focused_shop_tab_index() -> int:
var focused: Control = get_viewport().gui_get_focus_owner() var focused: Control = get_viewport().gui_get_focus_owner()
for tab_index: int in _shop_tabs.size(): for tab_index: int in _shop_tabs.size():
@ -430,11 +419,34 @@ func _configure_controller_focus() -> void:
if not visible or _cooler_page_active: if not visible or _cooler_page_active:
return return
_apply_shop_controller_zone_focus_modes() _apply_shop_controller_zone_focus_modes()
var candidates: Array[Control] = [] var candidates: Array[Control] = _active_shop_controller_controls()
_collect_controller_focusables(self, candidates)
ControllerFocusNavigationType.configure_spatial_neighbors(candidates) ControllerFocusNavigationType.configure_spatial_neighbors(candidates)
func _active_shop_controller_controls() -> Array[Control]:
var controls: Array[Control] = []
if _controller_zone == ControllerZone.TABS:
for tab: OrganizerTab in _shop_tabs:
if ControllerFocusNavigationType.is_focusable(tab):
controls.append(tab)
return controls
var active_root: Control = (
_upgrades_content
if _shop_section == ShopSection.UPGRADES
else _supplies_content
)
for node: Node in active_root.find_children(
"*", "BaseButton", true, false
):
var button := node as BaseButton
if ControllerFocusNavigationType.is_focusable(button):
controls.append(button)
var close_button := %CloseButton as Button
if ControllerFocusNavigationType.is_focusable(close_button):
controls.append(close_button)
return controls
func _apply_shop_controller_zone_focus_modes() -> void: func _apply_shop_controller_zone_focus_modes() -> void:
var tabs_active: bool = _controller_zone == ControllerZone.TABS var tabs_active: bool = _controller_zone == ControllerZone.TABS
for tab: OrganizerTab in _shop_tabs: for tab: OrganizerTab in _shop_tabs:
@ -459,22 +471,6 @@ func _set_descendant_button_focus_mode(
button.focus_mode = focus_mode button.focus_mode = focus_mode
func _collect_controller_focusables(
root: Node,
output: Array[Control],
) -> void:
for child: Node in root.get_children():
var control := child as Control
if control != null and not control.is_visible_in_tree():
continue
if (
ControllerFocusNavigationType.is_focusable(control)
and not control is ScrollBar
):
output.append(control)
_collect_controller_focusables(child, output)
func _update_shop_tab_selection() -> void: func _update_shop_tab_selection() -> void:
for tab_index: int in range(_shop_tabs.size()): for tab_index: int in range(_shop_tabs.size()):
_shop_tabs[tab_index].set_selected( _shop_tabs[tab_index].set_selected(

View file

@ -1268,6 +1268,10 @@ func _update_controller_menu_scroll(delta: float) -> void:
_controller_mapping_manager == null _controller_mapping_manager == null
or _virtual_mouse_active or _virtual_mouse_active
or is_input_mapping_capturing() or is_input_mapping_capturing()
or (
_player_menu.visible
and not _player_menu.allows_global_controller_scroll()
)
): ):
return return
var stick_y: float = _controller_menu_scroll_axis() var stick_y: float = _controller_menu_scroll_axis()

View file

@ -1,8 +1,8 @@
class_name InterfaceFontController class_name InterfaceFontController
extends Node extends Node
const FileDialogControllerNavigationType = preload( const FileDialogControllerType = preload(
"res://ui/file_dialog_controller_navigation.gd" "res://ui/file_dialog_controller.gd"
) )
const STANDARD_FONT: Font = preload("res://ui/fonts/Tuffy_Bold.otf") const STANDARD_FONT: Font = preload("res://ui/fonts/Tuffy_Bold.otf")
const COMPACT_FILE_DIALOG_LIMIT := Vector2i(800, 600) const COMPACT_FILE_DIALOG_LIMIT := Vector2i(800, 600)
@ -14,13 +14,19 @@ var _controller_text_entry_is_open: Callable
var _game_theme: Theme = preload("res://ui/game_theme.tres") var _game_theme: Theme = preload("res://ui/game_theme.tres")
var _utility_theme: Theme var _utility_theme: Theme
var _compact_file_dialog_theme: Theme var _compact_file_dialog_theme: Theme
var _tracked_file_dialogs: Dictionary[int, WeakRef] = {} var _file_dialog_controller: FileDialogControllerType
func _ready() -> void: func _ready() -> void:
enforce_standard_font() enforce_standard_font()
_utility_theme = _game_theme.duplicate(true) _utility_theme = _game_theme.duplicate(true)
_utility_theme.default_font = STANDARD_FONT _utility_theme.default_font = STANDARD_FONT
_file_dialog_controller = FileDialogControllerType.new()
add_child(_file_dialog_controller)
_file_dialog_controller.setup_text_entry(
_controller_text_entry_request,
_controller_text_entry_is_open,
)
func enforce_standard_font() -> void: func enforce_standard_font() -> void:
@ -35,6 +41,8 @@ func set_controller_text_entry_request(
) -> void: ) -> void:
_controller_text_entry_request = request _controller_text_entry_request = request
_controller_text_entry_is_open = is_open _controller_text_entry_is_open = is_open
if _file_dialog_controller != null:
_file_dialog_controller.setup_text_entry(request, is_open)
func set_readable_font_enabled(_enabled: bool) -> void: func set_readable_font_enabled(_enabled: bool) -> void:
@ -61,7 +69,7 @@ func apply_utility_theme(themed_node: Node) -> void:
func popup_file_dialog(dialog: FileDialog) -> void: func popup_file_dialog(dialog: FileDialog) -> void:
if dialog == null or not dialog.is_inside_tree(): if dialog == null or not dialog.is_inside_tree():
return return
_tracked_file_dialogs[dialog.get_instance_id()] = weakref(dialog) _file_dialog_controller.track_dialog(dialog)
var host_window: Window = dialog.get_parent().get_window() var host_window: Window = dialog.get_parent().get_window()
var host_size: Vector2i = host_window.size var host_size: Vector2i = host_window.size
var compact: bool = ( var compact: bool = (
@ -98,113 +106,6 @@ func popup_file_dialog(dialog: FileDialog) -> void:
) )
func _process(_delta: float) -> void:
if (
_controller_text_entry_is_open.is_valid()
and bool(_controller_text_entry_is_open.call())
):
return
for instance_id: int in _tracked_file_dialogs.keys():
var reference: WeakRef = _tracked_file_dialogs[instance_id]
var dialog := reference.get_ref() as FileDialog
if dialog == null or not is_instance_valid(dialog):
_tracked_file_dialogs.erase(instance_id)
continue
if not dialog.visible:
continue
FileDialogControllerNavigationType.configure(dialog)
_connect_file_dialog_text_controls(dialog)
func _input(event: InputEvent) -> void:
if (
_controller_text_entry_is_open.is_valid()
and bool(_controller_text_entry_is_open.call())
):
return
if not (event is InputEventJoypadButton or event is InputEventJoypadMotion):
return
var scope: Window = _active_file_dialog_scope()
if scope == null:
return
var focused: Control = scope.gui_get_focus_owner()
if focused == null:
return
var direction: Vector2 = _controller_direction(event)
var item_list := focused as ItemList
if (
item_list != null
and direction != Vector2.ZERO
and FileDialogControllerNavigationType.move_from_item_list(
item_list, direction
)
):
get_viewport().set_input_as_handled()
return
var button_event := event as InputEventJoypadButton
if (
button_event != null
and button_event.pressed
and event.is_action_pressed(&"ui_accept")
and (focused is LineEdit or focused is TextEdit)
and _controller_text_entry_request.is_valid()
and bool(_controller_text_entry_request.call(focused))
):
get_viewport().set_input_as_handled()
func _active_file_dialog_scope() -> Window:
for reference_value: WeakRef in _tracked_file_dialogs.values():
var dialog := reference_value.get_ref() as FileDialog
if dialog == null or not is_instance_valid(dialog) or not dialog.visible:
continue
return FileDialogControllerNavigationType.active_scope(dialog)
return null
func _controller_direction(event: InputEvent) -> Vector2:
if event.is_action_pressed(&"ui_up"):
return Vector2.UP
if event.is_action_pressed(&"ui_down"):
return Vector2.DOWN
if event.is_action_pressed(&"ui_left"):
return Vector2.LEFT
if event.is_action_pressed(&"ui_right"):
return Vector2.RIGHT
return Vector2.ZERO
func _connect_file_dialog_text_controls(dialog: FileDialog) -> void:
var scope: Window = FileDialogControllerNavigationType.active_scope(dialog)
for control: Control in (
FileDialogControllerNavigationType.interactive_controls(scope)
):
if not (control is LineEdit or control is TextEdit):
continue
var callback := _on_file_dialog_text_gui_input.bind(control)
if not control.gui_input.is_connected(callback):
control.gui_input.connect(callback)
func _on_file_dialog_text_gui_input(
event: InputEvent,
control: Control,
) -> void:
var button_event := event as InputEventJoypadButton
if (
button_event == null
or not button_event.pressed
or (
button_event.button_index != JOY_BUTTON_A
and not event.is_action_pressed("ui_accept")
)
or not _controller_text_entry_request.is_valid()
):
return
if bool(_controller_text_entry_request.call(control)):
control.accept_event()
func _finalize_compact_file_dialog( func _finalize_compact_file_dialog(
dialog: FileDialog, dialog: FileDialog,
host_size: Vector2i, host_size: Vector2i,

View file

@ -137,32 +137,96 @@ func handle_controller_input(event: InputEvent) -> bool:
func _refresh_controller_navigation() -> void: func _refresh_controller_navigation() -> void:
var controls: Array[Control] = [] var active_controls: Array[Control] = _active_controller_controls()
_collect_visible_controller_controls(self, controls) for control: Control in _all_controller_controls():
for control: Control in controls:
var button := control as BaseButton var button := control as BaseButton
control.focus_mode = ( control.focus_mode = (
Control.FOCUS_ALL Control.FOCUS_ALL
if _interactive and (button == null or not button.disabled) if (
_interactive
and control in active_controls
and control.is_visible_in_tree()
and (button == null or not button.disabled)
)
else Control.FOCUS_NONE else Control.FOCUS_NONE
) )
ControllerFocusNavigation.configure_spatial_neighbors(controls) ControllerFocusNavigation.configure_spatial_neighbors(active_controls)
if _compose != null and _compose.visible: if _compose != null and _compose.visible:
_configure_compose_controller_navigation() _configure_compose_controller_navigation()
elif _inbox != null and _inbox.visible: elif _inbox != null and _inbox.visible:
_configure_inbox_controller_navigation() _configure_inbox_controller_navigation()
elif _letter != null and _letter.visible: elif _letter != null and _letter.visible:
_configure_letter_controller_navigation() _configure_letter_controller_navigation()
if not _interactive or controls.is_empty(): if not _interactive or active_controls.is_empty():
return return
var focus_owner: Control = get_viewport().gui_get_focus_owner() var focus_owner: Control = get_viewport().gui_get_focus_owner()
if focus_owner == null or not is_ancestor_of(focus_owner): if focus_owner == null or not is_ancestor_of(focus_owner):
controls.sort_custom(func(first: Control, second: Control) -> bool: active_controls.front().grab_focus()
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 func _active_controller_controls() -> Array[Control]:
) if _compose != null and _compose.visible:
controls.front().grab_focus() return _compose_controller_controls()
if _letter != null and _letter.visible:
return _letter_controller_controls()
return _inbox_controller_controls()
func _all_controller_controls() -> Array[Control]:
var controls: Array[Control] = _inbox_controller_controls()
controls.append_array(_compose_controller_controls())
controls.append_array(_letter_controller_controls())
return controls
func _inbox_controller_controls() -> Array[Control]:
var controls: Array[Control] = []
_append_controller_control(controls, _archive_view_button)
_append_controller_control(controls, _send_mail_button)
if _inbox_list != null:
for child: Node in _inbox_list.get_children():
_append_controller_control(controls, child as Button)
return controls
func _compose_controller_controls() -> Array[Control]:
var controls: Array[Control] = []
for control: Control in [
_greeting,
_recipient,
_attachment_kind,
_body,
_attachment_choice,
_salutation,
_amount_minus,
_attachment_amount,
_amount_plus,
_compose_cancel,
_send_button,
]:
_append_controller_control(controls, control)
return controls
func _letter_controller_controls() -> Array[Control]:
var controls: Array[Control] = []
for control: Control in [
_accept,
_decline,
_letter_close,
_archive,
_delete,
]:
_append_controller_control(controls, control)
return controls
func _append_controller_control(
controls: Array[Control],
control: Control,
) -> void:
if control != null and control.is_visible_in_tree():
controls.append(control)
func _configure_inbox_controller_navigation() -> void: func _configure_inbox_controller_navigation() -> void:
@ -358,22 +422,6 @@ func _controller_focus_eligible(control: Control) -> bool:
return button == null or not button.disabled return button == null or not button.disabled
func _collect_visible_controller_controls(
root: Node,
output: Array[Control],
) -> void:
for child: Node in root.get_children():
var control := child as Control
if control != null and not control.is_visible_in_tree():
continue
if (
control != null
and (control is BaseButton or control is LineEdit or control is TextEdit)
):
output.append(control)
_collect_visible_controller_controls(child, output)
func _build_ui() -> void: func _build_ui() -> void:
var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self) var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self)
var root := Control.new() var root := Control.new()
@ -1041,12 +1089,6 @@ func _attachment_text(attachment: Dictionary) -> String:
return "Invalid gift." return "Invalid gift."
func _focus_first() -> void:
var buttons := _inbox_list.find_children("*", "Button", true, false)
if not buttons.is_empty():
(buttons.front() as Button).grab_focus()
func _style_controls(root: Node) -> void: func _style_controls(root: Node) -> void:
for node: Node in root.find_children("*", "", true, false): for node: Node in root.find_children("*", "", true, false):
if node is BaseButton: if node is BaseButton:

View file

@ -347,7 +347,7 @@ func _on_save_pressed() -> void:
true, true,
) )
_set_mode(Mode.SAVED) _set_mode(Mode.SAVED)
_select_entry_id(existing.entry_id) _restore_entry_selection_and_focus(0, existing.entry_id)
return return
_address.text = endpoint.normalized_display _address.text = endpoint.normalized_display
_name_edit.text = ( _name_edit.text = (
@ -389,8 +389,7 @@ func _commit_name_entry() -> void:
_clear_edit_state() _clear_edit_state()
_mode = Mode.SAVED _mode = Mode.SAVED
_refresh_entries() _refresh_entries()
_select_entry_id(saved.entry_id) _restore_entry_selection_and_focus(0, saved.entry_id)
_refresh()
func _on_edit_pressed() -> void: func _on_edit_pressed() -> void:
@ -412,8 +411,7 @@ func _on_favorite_pressed() -> void:
var entry_id: String = _selected_entry.entry_id var entry_id: String = _selected_entry.entry_id
_saved_servers.set_favorite(entry_id, not _selected_entry.favorite) _saved_servers.set_favorite(entry_id, not _selected_entry.favorite)
_refresh_entries() _refresh_entries()
_select_entry_id(entry_id) _restore_entry_selection_and_focus(0, entry_id)
_refresh()
func _on_delete_pressed() -> void: func _on_delete_pressed() -> void:
@ -437,10 +435,12 @@ func _confirm_delete() -> void:
if _selected_entry == null or not _delete_armed: if _selected_entry == null or not _delete_armed:
_cancel_delete() _cancel_delete()
return return
var removed_index: int = _visible_entries.find(_selected_entry)
var selected_id: String = _selected_entry.entry_id
var removed: bool = ( var removed: bool = (
_saved_servers.remove_recent_entry(_selected_entry.entry_id) _saved_servers.remove_recent_entry(selected_id)
if _mode == Mode.RECENT if _mode == Mode.RECENT
else _saved_servers.remove_saved_entry(_selected_entry.entry_id) else _saved_servers.remove_saved_entry(selected_id)
) )
_set_status( _set_status(
( (
@ -452,11 +452,13 @@ func _confirm_delete() -> void:
else "Could not remove the local entry.", else "Could not remove the local entry.",
not removed, not removed,
) )
_selected_entry = null
_delete_armed = false _delete_armed = false
_refresh_entries()
_refresh()
_delete_confirmation.hide_page() _delete_confirmation.hide_page()
_refresh_entries()
_restore_entry_selection_and_focus(
maxi(removed_index, 0),
"" if removed else selected_id,
)
func _cancel_delete() -> void: func _cancel_delete() -> void:
@ -478,12 +480,51 @@ func _on_list_item_selected(index: int) -> void:
_refresh() _refresh()
func _select_entry_id(entry_id: String) -> void: func _select_entry_id(entry_id: String) -> bool:
for index: int in range(_visible_entries.size()): for index: int in range(_visible_entries.size()):
if _visible_entries[index].entry_id == entry_id: if _visible_entries[index].entry_id == entry_id:
_server_list.select(index) return _select_entry_index(index)
_selected_entry = _visible_entries[index] return false
return
func _select_entry_index(index: int) -> bool:
if index < 0 or index >= _visible_entries.size():
return false
_server_list.select(index)
_server_list.ensure_current_is_visible()
_selected_entry = _visible_entries[index]
return true
func _restore_entry_selection_and_focus(
preferred_index: int,
preferred_id: String = "",
) -> void:
var selected: bool = (
not preferred_id.is_empty() and _select_entry_id(preferred_id)
)
if not selected and not _visible_entries.is_empty():
selected = _select_entry_index(
clampi(preferred_index, 0, _visible_entries.size() - 1)
)
if not selected:
_selected_entry = null
_server_list.deselect_all()
_refresh()
_defer_focus_control(
_server_list if selected else _current_mode_button()
)
func _current_mode_button() -> Button:
match _mode:
Mode.DIRECT:
return _direct_button
Mode.SAVED:
return _saved_button
Mode.RECENT:
return _recent_button
return _discover_button
func _select_discovery_index(index: int) -> bool: func _select_discovery_index(index: int) -> bool:
@ -1041,10 +1082,18 @@ func _on_peer_count_changed(player_count: int, max_players: int) -> void:
func _on_store_changed() -> void: func _on_store_changed() -> void:
var selected_index: int = _visible_entries.find(_selected_entry)
var selected_id: String = ( var selected_id: String = (
_selected_entry.entry_id if _selected_entry != null else "" _selected_entry.entry_id if _selected_entry != null else ""
) )
_refresh_entries() _refresh_entries()
if not selected_id.is_empty(): _selected_entry = null
_select_entry_id(selected_id) if (
not selected_id.is_empty()
and not _select_entry_id(selected_id)
and not _visible_entries.is_empty()
):
_select_entry_index(
clampi(maxi(selected_index, 0), 0, _visible_entries.size() - 1)
)
_refresh() _refresh()

View file

@ -341,6 +341,13 @@ func _preserve_native_text_focus(control: Control) -> void:
func _can_edit(control: Control) -> bool: func _can_edit(control: Control) -> bool:
if (
control == null
or not is_instance_valid(control)
or not control.is_inside_tree()
or not control.is_visible_in_tree()
):
return false
if control is LineEdit: if control is LineEdit:
return (control as LineEdit).editable return (control as LineEdit).editable
if control is TextEdit: if control is TextEdit:

View file

@ -3,8 +3,6 @@ extends Control
const INPUT_OWNER: StringName = &"game_menu" const INPUT_OWNER: StringName = &"game_menu"
const PAUSE_DESKTOP_REFERENCE_SIZE: Vector2 = Vector2(1280.0, 720.0) const PAUSE_DESKTOP_REFERENCE_SIZE: Vector2 = Vector2(1280.0, 720.0)
const PAUSE_COMPACT_REFERENCE_SIZE: Vector2 = Vector2(640.0, 480.0)
const COMPACT_HEIGHT_THRESHOLD: float = 560.0
const VISIBILITY_FADE_DURATION: float = 0.55 const VISIBILITY_FADE_DURATION: float = 0.55
const PlayerType = preload("res://player/player.gd") const PlayerType = preload("res://player/player.gd")
const SaveManagerType = preload("res://save/player_save_manager.gd") const SaveManagerType = preload("res://save/player_save_manager.gd")

View file

@ -76,9 +76,6 @@ focus_paths = Array[NodePath]([NodePath("BubbleCluster/ResumeButton"), NodePath(
initial_focus_path = NodePath("BubbleCluster/ResumeButton") initial_focus_path = NodePath("BubbleCluster/ResumeButton")
back_focus_path = NodePath("BubbleCluster/ResumeButton") back_focus_path = NodePath("BubbleCluster/ResumeButton")
maximum_layout_size = Vector2(720, 520) maximum_layout_size = Vector2(720, 520)
compact_maximum_layout_size = Vector2(544, 400)
compact_width_threshold = 680.0
compact_height_threshold = 500.0
[node name="BubbleCluster" type="Control" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage"] [node name="BubbleCluster" type="Control" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage"]
layout_mode = 0 layout_mode = 0

View file

@ -51,12 +51,6 @@ const BubbleButtonType = preload(
const BubbleClusterType = preload( const BubbleClusterType = preload(
"res://ui/components/bubble_menu/bubble_cluster.gd" "res://ui/components/bubble_menu/bubble_cluster.gd"
) )
const BubbleContentShellType = preload(
"res://ui/components/bubble_menu/bubble_content_shell.gd"
)
const BubbleStatusBubbleType = preload(
"res://ui/components/bubble_menu/bubble_status_bubble.gd"
)
const NotepadInkActionType = preload( const NotepadInkActionType = preload(
"res://ui/components/bubble_menu/notepad_ink_action.gd" "res://ui/components/bubble_menu/notepad_ink_action.gd"
) )
@ -78,17 +72,7 @@ const BagItemSpriteScene = preload(
const BagStorageSlotType = preload( const BagStorageSlotType = preload(
"res://ui/components/bubble_menu/bag_storage_slot.gd" "res://ui/components/bubble_menu/bag_storage_slot.gd"
) )
const LogbookEntryType = preload(
"res://ui/components/bubble_menu/logbook_entry.gd"
)
const LogbookEntryScene = preload(
"res://ui/components/bubble_menu/logbook_entry.tscn"
)
const LOGBOOK_PAGE_DURATION: float = 0.18
const DESKTOP_REFERENCE_SIZE := Vector2(1280.0, 720.0) const DESKTOP_REFERENCE_SIZE := Vector2(1280.0, 720.0)
const COMPACT_REFERENCE_SIZE := Vector2(640.0, 480.0)
const COMPACT_HEIGHT_THRESHOLD: float = 560.0
const NAVIGATION_PRESENTATION_SCALE: float = 0.60 const NAVIGATION_PRESENTATION_SCALE: float = 0.60
const NAVIGATION_CANONICAL_POSITION := Vector2(424.0, 44.0) const NAVIGATION_CANONICAL_POSITION := Vector2(424.0, 44.0)
const NAVIGATION_SELECTED_SCALE: float = 1.02 const NAVIGATION_SELECTED_SCALE: float = 1.02
@ -236,18 +220,6 @@ const CONTROLLER_PICKUP_HOLD_SECONDS: float = 0.42
@onready var _mail_page: MailPage = %MailPage @onready var _mail_page: MailPage = %MailPage
@onready var _profile_page: ProfilePage = %ProfilePage @onready var _profile_page: ProfilePage = %ProfilePage
@onready var _players_page: PlayersPage = %PlayersPage @onready var _players_page: PlayersPage = %PlayersPage
@onready var _book_backing: PanelContainer = %BookBacking
@onready var _book_spread: BoxContainer = %BookSpread
@onready var _left_page: PanelContainer = %LeftPage
@onready var _right_page: PanelContainer = %RightPage
@onready var _left_heading: Label = %LeftHeading
@onready var _right_heading: Label = %RightHeading
@onready var _left_entry_field: BoxContainer = %LeftEntryField
@onready var _right_entry_field: BoxContainer = %RightEntryField
@onready var _logbook_empty_state: Label = %LogbookEmptyState
@onready var _logbook_previous: BubbleButtonType = %LogbookPrevious
@onready var _logbook_next: BubbleButtonType = %LogbookNext
@onready var _logbook_page_status: BubbleStatusBubbleType = %LogbookPageStatus
@onready var _inventory_tab: BubbleButtonType = %InventoryTab @onready var _inventory_tab: BubbleButtonType = %InventoryTab
@onready var _logbook_tab: BubbleButtonType = %LogbookTab @onready var _logbook_tab: BubbleButtonType = %LogbookTab
@onready var _the_net_tab: BubbleButtonType = %TheNetTab @onready var _the_net_tab: BubbleButtonType = %TheNetTab
@ -256,42 +228,12 @@ const CONTROLLER_PICKUP_HOLD_SECONDS: float = 0.42
@onready var _players_tab: BubbleButtonType = %PlayersTab @onready var _players_tab: BubbleButtonType = %PlayersTab
@onready var _mail_unread_badge: Label = %MailUnreadBadge @onready var _mail_unread_badge: Label = %MailUnreadBadge
@onready var _close_button: BubbleButtonType = %CloseButton @onready var _close_button: BubbleButtonType = %CloseButton
@onready var _content_shell: BubbleContentShellType = %MenuPanel
@onready var _content_stage: Control = %Content
@onready var _cooler_scroll: ScrollContainer = %CoolerScroll @onready var _cooler_scroll: ScrollContainer = %CoolerScroll
@onready var _cooler_host: Control = %CoolerHost @onready var _cooler_host: Control = %CoolerHost
@onready var _wallet_balance: CurrencyAmount = %WalletBalance
@onready var _header: Control = %Header
@onready var _separator: Control = %Separator
@onready var _wallet_status: BubbleStatusBubbleType = %WalletStatus
@onready var _capacity_status: BubbleStatusBubbleType = %CapacityStatus
@onready var _held_value_status: BubbleStatusBubbleType = %HeldValueStatus
@onready var _selection_status: BubbleStatusBubbleType = %SelectionStatus
@onready var _offer_status: BubbleStatusBubbleType = %OfferStatus
@onready var _inventory_section: Control = %InventorySection
@onready var _bag_section: Control = %BagSection
@onready var _bag_empty: Label = %BagEmpty
@onready var _bag_grid: GridContainer = %BagGrid
@onready var _bag_list: Control = %BagList
@onready var _bag_detail: Control = %BagDetail
@onready var _bag_detail_texture: TextureRect = %BagDetailTexture
@onready var _bag_detail_name: Label = %BagDetailName
@onready var _bag_detail_data: Label = %BagDetailData
@onready var _sort_option: OptionButton = %SortOption
@onready var _sort_direction: Button = %SortDirection
@onready var _held_value: CurrencyAmount = %HeldValue
@onready var _cooler_count: Label = %CoolerCount
@onready var _inventory_empty: Label = %InventoryEmpty
@onready var _selection_summary: Label = %SelectionSummary
@onready var _favorite_button: Button = %FavoriteButton
@onready var _sell_button: Button = %SellButton
@onready var _sale_unavailable: Label = %SaleUnavailable
@onready var _transaction_feedback: RichTextLabel = %TransactionFeedback
@onready var _sale_confirmation: PanelContainer = %SaleConfirmation @onready var _sale_confirmation: PanelContainer = %SaleConfirmation
@onready var _confirmation_message: RichTextLabel = %ConfirmationMessage @onready var _confirmation_message: RichTextLabel = %ConfirmationMessage
@onready var _confirm_sale_button: Button = %ConfirmSaleButton @onready var _confirm_sale_button: Button = %ConfirmSaleButton
@onready var _cancel_sale_button: Button = %CancelSaleButton @onready var _cancel_sale_button: Button = %CancelSaleButton
@onready var _logbook_empty: Label = %LogbookEmpty
var _compact_layout: bool = false var _compact_layout: bool = false
var _player: PlayerType var _player: PlayerType
@ -361,8 +303,6 @@ var _transition_generation: int = 0
var _page_transition_generation: int = 0 var _page_transition_generation: int = 0
var _transitioning: bool = false var _transitioning: bool = false
var _page_transitioning: bool = false var _page_transitioning: bool = false
var _presentation_rest_position: Vector2 = Vector2.ZERO
var _content_rest_position: Vector2 = Vector2.ZERO
var _cooler_rest_position: Vector2 = Vector2.ZERO var _cooler_rest_position: Vector2 = Vector2.ZERO
var _bag_rest_position: Vector2 = Vector2.ZERO var _bag_rest_position: Vector2 = Vector2.ZERO
var _tackle_rest_position: Vector2 = Vector2.ZERO var _tackle_rest_position: Vector2 = Vector2.ZERO
@ -382,12 +322,6 @@ var _bag_slot_nodes: Array[BagStorageSlotType] = []
var _sorted_bag_items: Array[OwnedItemType] = [] var _sorted_bag_items: Array[OwnedItemType] = []
var _bag_drag_active: bool = false var _bag_drag_active: bool = false
var _motion_elapsed: float = 0.0 var _motion_elapsed: float = 0.0
var _logbook_species: Array[FishDataType] = []
var _logbook_current_page: int = 0
var _logbook_page_count: int = 1
var _logbook_page_transitioning: bool = false
var _logbook_page_generation: int = 0
var _logbook_page_tween: Tween
func _ready() -> void: func _ready() -> void:
@ -410,22 +344,14 @@ func _ready() -> void:
_profile_tab.pressed.connect(_show_section.bind(Section.PROFILE)) _profile_tab.pressed.connect(_show_section.bind(Section.PROFILE))
_players_tab.pressed.connect(_show_section.bind(Section.PLAYERS)) _players_tab.pressed.connect(_show_section.bind(Section.PLAYERS))
_close_button.pressed.connect(close_menu) _close_button.pressed.connect(close_menu)
_sort_option.item_selected.connect(_on_sort_selected.bind(_sort_option))
_sort_direction.pressed.connect(_on_sort_direction_pressed)
_cooler_sort_option.item_selected.connect(_on_cooler_sort_selected) _cooler_sort_option.item_selected.connect(_on_cooler_sort_selected)
_cooler_sort_direction.pressed.connect(_on_sort_direction_pressed) _cooler_sort_direction.pressed.connect(_on_sort_direction_pressed)
_favorite_button.pressed.connect(_on_favorite_pressed)
_sell_button.pressed.connect(_on_sell_pressed)
_favorite_bubble.pressed.connect(_on_favorite_pressed) _favorite_bubble.pressed.connect(_on_favorite_pressed)
_sell_bubble.pressed.connect(_on_sell_pressed) _sell_bubble.pressed.connect(_on_sell_pressed)
_sell_all_bubble.pressed.connect(_on_sell_all_pressed) _sell_all_bubble.pressed.connect(_on_sell_all_pressed)
_confirm_sale_button.pressed.connect(_on_confirm_sale_pressed) _confirm_sale_button.pressed.connect(_on_confirm_sale_pressed)
_cancel_sale_button.pressed.connect(_close_sale_confirmation) _cancel_sale_button.pressed.connect(_close_sale_confirmation)
_configure_sale_confirmation_focus() _configure_sale_confirmation_focus()
_sort_option.add_item("catch order", SortMode.CATCH_ORDER)
_sort_option.add_item("name", SortMode.NAME)
_sort_option.add_item("rarity", SortMode.RARITY)
_sort_option.select(SortMode.CATCH_ORDER)
_cooler_sort_option.add_item("catch order", SortMode.CATCH_ORDER) _cooler_sort_option.add_item("catch order", SortMode.CATCH_ORDER)
_cooler_sort_option.add_item("name", SortMode.NAME) _cooler_sort_option.add_item("name", SortMode.NAME)
_cooler_sort_option.add_item("rarity", SortMode.RARITY) _cooler_sort_option.add_item("rarity", SortMode.RARITY)
@ -446,7 +372,6 @@ func _ready() -> void:
# Keep the exposed tab hitboxes above the full-page roots. The individual # Keep the exposed tab hitboxes above the full-page roots. The individual
# main panels retain a higher z-index and cover the tab bodies below y=166. # main panels retain a higher z-index and cover the tab bodies below y=166.
_inventory_sub_tabs.move_to_front() _inventory_sub_tabs.move_to_front()
_apply_cooler_control_styles()
_apply_cooler_wall_styles() _apply_cooler_wall_styles()
_apply_cooler_notepad_style() _apply_cooler_notepad_style()
_cooler_water_surface.resized.connect(_update_cooler_water_mask) _cooler_water_surface.resized.connect(_update_cooler_water_mask)
@ -626,6 +551,12 @@ 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)
func allows_global_controller_scroll() -> bool:
# The profile page owns the right stick for preview orbit, zoom, and its
# color picker. The shared menu scroller must not interpret that axis too.
return _current_section != Section.PROFILE
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
@ -1768,12 +1699,6 @@ func _show_section_immediate(section: Section) -> void:
_mail_page.visible = section == Section.MAIL _mail_page.visible = section == Section.MAIL
_profile_page.visible = section == Section.PROFILE _profile_page.visible = section == Section.PROFILE
_players_page.visible = section == Section.PLAYERS _players_page.visible = section == Section.PLAYERS
_content_shell.visible = false
_inventory_section.visible = false
_bag_section.visible = false
_content_shell.set_background_visible(true)
_header.visible = section != Section.COOLER
_separator.visible = section != Section.COOLER
if section == Section.COOLER: if section == Section.COOLER:
_refresh_inventory() _refresh_inventory()
else: else:
@ -1787,7 +1712,6 @@ func _show_section_immediate(section: Section) -> void:
_refresh_bag() _refresh_bag()
_refresh_tackle_box() _refresh_tackle_box()
if section != Section.LOGBOOK: if section != Section.LOGBOOK:
_cancel_logbook_page_transition(true)
_catalog_logbook.deactivate() _catalog_logbook.deactivate()
else: else:
_catalog_logbook.activate() _catalog_logbook.activate()
@ -1874,12 +1798,9 @@ func _focus_current_section() -> void:
if _shop_cooler_context_active: if _shop_cooler_context_active:
_focus_shop_cooler() _focus_shop_cooler()
return return
_reserve_main_navigation_for_page_switching() var candidates: Array[Control] = _inventory_content_focus_candidates()
_reserve_visible_secondary_navigation()
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
var candidates: Array[Control] = []
_collect_focusable_content(self, navigation_cluster, candidates)
if candidates.is_empty(): if candidates.is_empty():
_enter_inventory_tabs_zone()
return return
ControllerFocusNavigationType.configure_spatial_neighbors(candidates) ControllerFocusNavigationType.configure_spatial_neighbors(candidates)
candidates.sort_custom(func(first: Control, second: Control) -> bool: candidates.sort_custom(func(first: Control, second: Control) -> bool:
@ -1890,23 +1811,22 @@ func _focus_current_section() -> void:
candidates[0].grab_focus() candidates[0].grab_focus()
func _collect_focusable_content( func _inventory_content_focus_candidates() -> Array[Control]:
root: Node, var candidates: Array[Control] = []
navigation_cluster: Control, match _current_section:
output: Array[Control], Section.COOLER:
) -> void: for fish_node: CoolerFishSpriteType in _fish_nodes.values():
for child: Node in root.get_children(): if ControllerFocusNavigationType.is_focusable(fish_node):
if child == navigation_cluster: candidates.append(fish_node)
continue Section.BAG:
var control := child as Control for item_node: BagItemSpriteType in _bag_item_nodes.values():
if control != null and not control.is_visible_in_tree(): if ControllerFocusNavigationType.is_focusable(item_node):
continue candidates.append(item_node)
if ( Section.TACKLE_BOX:
ControllerFocusNavigationType.is_focusable(control) for tackle_button: Button in _tackle_item_buttons.values():
and not control is ScrollBar if ControllerFocusNavigationType.is_focusable(tackle_button):
): candidates.append(tackle_button)
output.append(control) return candidates
_collect_focusable_content(child, navigation_cluster, output)
func _process(delta: float) -> void: func _process(delta: float) -> void:
@ -2573,35 +2493,6 @@ func _on_active_lure_changed(_item_id: StringName) -> void:
_update_tackle_detail() _update_tackle_detail()
func _apply_cooler_control_styles() -> void:
var profile: BubbleMenuProfile = _inventory_tab.profile
if profile == null:
return
_sort_option.add_theme_stylebox_override(
"normal",
profile.make_normal_style(),
)
_sort_option.add_theme_stylebox_override(
"hover",
profile.make_hover_style(),
)
_sort_option.add_theme_stylebox_override(
"focus",
profile.make_hover_style(),
)
_sort_option.add_theme_stylebox_override(
"pressed",
profile.make_pressed_style(),
)
_sort_option.add_theme_color_override("font_color", profile.text_color)
_sort_option.add_theme_color_override(
"font_hover_color",
profile.text_hover_color,
)
_sort_option.add_theme_color_override(
"font_focus_color",
profile.text_hover_color,
)
func _apply_cooler_wall_styles() -> void: func _apply_cooler_wall_styles() -> void:
var outer := StyleBoxFlat.new() var outer := StyleBoxFlat.new()
outer.bg_color = Color(0.76, 0.9, 0.96, 1.0) outer.bg_color = Color(0.76, 0.9, 0.96, 1.0)
@ -2708,59 +2599,6 @@ func _apply_bag_styles() -> void:
inner.set_border_width_all(0) inner.set_border_width_all(0)
inner.set_corner_radius_all(INVENTORY_INNER_CORNER_RADIUS) inner.set_corner_radius_all(INVENTORY_INNER_CORNER_RADIUS)
_bag_inner_liner.add_theme_stylebox_override("panel", inner) _bag_inner_liner.add_theme_stylebox_override("panel", inner)
func _apply_logbook_styles() -> void:
var backing := StyleBoxFlat.new()
backing.bg_color = Color(0.54, 0.42, 0.27, 1.0)
backing.border_color = Color(0.72, 0.59, 0.39, 1.0)
backing.set_border_width_all(6)
backing.corner_radius_top_left = 42
backing.corner_radius_top_right = 34
backing.corner_radius_bottom_right = 46
backing.corner_radius_bottom_left = 36
_book_backing.add_theme_stylebox_override("panel", backing)
var left_paper := StyleBoxFlat.new()
left_paper.bg_color = Color(0.95, 0.91, 0.79, 1.0)
left_paper.border_color = Color(0.80, 0.70, 0.52, 1.0)
left_paper.set_border_width_all(3)
left_paper.corner_radius_top_left = 30
left_paper.corner_radius_bottom_left = 38
left_paper.corner_radius_top_right = 8
left_paper.corner_radius_bottom_right = 6
_left_page.add_theme_stylebox_override("panel", left_paper)
var right_paper := StyleBoxFlat.new()
right_paper.bg_color = Color(0.925, 0.875, 0.74, 1.0)
right_paper.border_color = Color(0.77, 0.66, 0.48, 1.0)
right_paper.set_border_width_all(3)
right_paper.corner_radius_top_left = 8
right_paper.corner_radius_bottom_left = 6
right_paper.corner_radius_top_right = 34
right_paper.corner_radius_bottom_right = 40
_right_page.add_theme_stylebox_override("panel", right_paper)
for label: Label in [_left_heading, _right_heading, _logbook_empty_state]:
label.add_theme_color_override(
"font_color",
Color(0.22, 0.16, 0.09, 1.0),
)
var page_profile: BubbleMenuProfile = _logbook_previous.profile
if page_profile != null:
var disabled_style: StyleBoxFlat = page_profile.make_normal_style()
disabled_style.bg_color.a = 0.82
disabled_style.border_color.a = 0.72
for control: BubbleButtonType in [
_logbook_previous,
_logbook_next,
]:
control.add_theme_stylebox_override(
"disabled",
disabled_style.duplicate(),
)
control.add_theme_color_override(
"font_disabled_color",
Color(0.035, 0.145, 0.22, 0.68),
)
_logbook_page_status.set_content("pages", "1 / 1")
func _update_navigation_selection() -> void: func _update_navigation_selection() -> void:
_set_navigation_target(_current_section) _set_navigation_target(_current_section)
@ -2790,9 +2628,6 @@ func _update_shell_layout() -> void:
_presentation_scale_root.scale = Vector2.ONE _presentation_scale_root.scale = Vector2.ONE
_presentation_scale_root.position = Vector2.ZERO _presentation_scale_root.position = Vector2.ZERO
_presentation_scale_root.pivot_offset = reference_size * 0.5 _presentation_scale_root.pivot_offset = reference_size * 0.5
_content_shell.position = Vector2(14.0, 92.0) if compact else Vector2(42.0, 104.0)
_content_shell.size = Vector2(612.0, 286.0) if compact else Vector2(1196.0, 478.0)
_presentation_rest_position = _content_shell.position
var navigation_size := ( var navigation_size := (
Vector2(840.0, 75.0) if compact else Vector2(840.0, 100.0) Vector2(840.0, 75.0) if compact else Vector2(840.0, 100.0)
) )
@ -2982,13 +2817,7 @@ func _update_shell_layout() -> void:
520.0 if compact else 800.0, 520.0 if compact else 800.0,
512.0 if compact else 420.0, 512.0 if compact else 420.0,
) )
_bag_grid.columns = 2 if compact else 3
_bag_list.custom_minimum_size.x = 300.0 if compact else 360.0
_bag_detail.custom_minimum_size.x = 176.0 if compact else 220.0
_content_stage.custom_minimum_size.y = 220.0 if compact else 260.0
_content_rest_position = _content_stage.position
if not _transitioning: if not _transitioning:
_content_shell.position = _presentation_rest_position
_cooler_page.position = _cooler_rest_position _cooler_page.position = _cooler_rest_position
_bag_page.position = _bag_rest_position _bag_page.position = _bag_rest_position
_tackle_box_page.position = _tackle_rest_position _tackle_box_page.position = _tackle_rest_position
@ -3005,8 +2834,6 @@ func _update_shell_layout() -> void:
_mail_page.modulate.a = 1.0 _mail_page.modulate.a = 1.0
_profile_page.modulate.a = 1.0 _profile_page.modulate.a = 1.0
_players_page.modulate.a = 1.0 _players_page.modulate.a = 1.0
if not _page_transitioning:
_content_stage.position = _content_rest_position
_layout_cooler_fish(false) _layout_cooler_fish(false)
_layout_bag_items() _layout_bag_items()
@ -3177,7 +3004,6 @@ func _finish_close(
) -> void: ) -> void:
_cancel_presentation_tween() _cancel_presentation_tween()
_cancel_page_tween() _cancel_page_tween()
_cancel_logbook_page_transition(true)
_the_net_page.deactivate() _the_net_page.deactivate()
_transitioning = false _transitioning = false
_page_transitioning = false _page_transitioning = false
@ -3243,7 +3069,6 @@ func _get_section_rest_position(section: Section) -> Vector2:
func _begin_page_transition(section: Section) -> void: func _begin_page_transition(section: Section) -> void:
_cancel_logbook_page_transition(true)
_page_transition_generation += 1 _page_transition_generation += 1
_cancel_page_tween() _cancel_page_tween()
_page_transitioning = true _page_transitioning = true
@ -3354,11 +3179,6 @@ func _set_shell_interactive(interactive: bool) -> void:
func _set_content_interactive(interactive: bool) -> void: func _set_content_interactive(interactive: bool) -> void:
_content_interactive_enabled = interactive _content_interactive_enabled = interactive
_content_stage.mouse_filter = (
Control.MOUSE_FILTER_PASS
if interactive
else Control.MOUSE_FILTER_IGNORE
)
_cooler_page.mouse_filter = ( _cooler_page.mouse_filter = (
Control.MOUSE_FILTER_PASS Control.MOUSE_FILTER_PASS
if interactive and _current_section == Section.COOLER if interactive and _current_section == Section.COOLER
@ -3499,19 +3319,8 @@ func _reset_page_transition_visuals() -> void:
_page_outgoing_content_root = null _page_outgoing_content_root = null
func _on_sort_selected(index: int, source: OptionButton) -> void:
var selected_id: int = source.get_item_id(index)
if selected_id < SortMode.CATCH_ORDER or selected_id > SortMode.RARITY:
return
_sort_mode = selected_id as SortMode
_sort_option.select(_sort_mode)
_cooler_sort_option.select(_sort_mode)
_refresh_inventory()
func _on_cooler_sort_selected(sort_id: int) -> void: func _on_cooler_sort_selected(sort_id: int) -> void:
_sort_mode = sort_id as SortMode _sort_mode = sort_id as SortMode
_sort_option.select(_sort_mode)
_cooler_sort_option.select(_sort_mode) _cooler_sort_option.select(_sort_mode)
_refresh_inventory() _refresh_inventory()
@ -3541,7 +3350,6 @@ func _update_sort_direction_text() -> void:
direction_text = ( direction_text = (
"high to low" if _sort_descending else "low to high" "high to low" if _sort_descending else "low to high"
) )
_sort_direction.text = direction_text
_cooler_sort_direction.text = direction_text _cooler_sort_direction.text = direction_text
@ -3592,7 +3400,6 @@ func _refresh_bag() -> void:
owned_items = filtered_items owned_items = filtered_items
owned_items.sort_custom(_sort_bag_storage_items) owned_items.sort_custom(_sort_bag_storage_items)
_sorted_bag_items = owned_items _sorted_bag_items = owned_items
_bag_empty.visible = false
_bag_empty_state.visible = false _bag_empty_state.visible = false
_bag_empty_state.text = ( _bag_empty_state.text = (
"No equipment in your Bag." "No equipment in your Bag."
@ -3932,23 +3739,8 @@ func _update_bag_detail() -> void:
item_state = "equippable" item_state = "equippable"
elif item.usable: elif item.usable:
item_state = "usable" item_state = "usable"
_bag_detail_texture.texture = null
_bag_detail_name.text = item.display_name if item != null else ""
_bag_detail_data.text = (
"%s\nquantity: %d\n%s\n%s\n%s"
% [
_item_category_display_name(item),
quantity,
item_state,
hotbar_assignment,
item.description,
]
if item != null
else "select a bag item for details."
)
_bag_sprite_detail_texture.texture = null _bag_sprite_detail_texture.texture = null
if item != null: if item != null:
_bag_detail_texture.texture = item.icon
_bag_sprite_detail_texture.texture = item.icon _bag_sprite_detail_texture.texture = item.icon
_bag_sprite_detail_name.text = ( _bag_sprite_detail_name.text = (
item.display_name if item != null else "" item.display_name if item != null else ""
@ -4065,23 +3857,11 @@ func _refresh_economy_summary() -> void:
if _inventory != null if _inventory != null
else 0 else 0
) )
_wallet_balance.set_amount(balance)
_wallet_status.set_currency_amount("wallet", balance)
_notepad_wallet_value.set_amount(balance) _notepad_wallet_value.set_amount(balance)
_capacity_status.set_content("cooler", "%d / %d" % [
_inventory.get_all_catches().size() if _inventory != null else 0,
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
])
_notepad_capacity_value.text = "%d / %d" % [ _notepad_capacity_value.text = "%d / %d" % [
_inventory.get_all_catches().size() if _inventory != null else 0, _inventory.get_all_catches().size() if _inventory != null else 0,
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0, _cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
] ]
_held_value_status.set_currency_amount("held value", held_total)
_held_value.set_amount(held_total)
_cooler_count.text = "%d / %d" % [
_inventory.get_all_catches().size() if _inventory != null else 0,
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
]
func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void: func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void:
@ -4098,7 +3878,6 @@ func _refresh_inventory() -> void:
catches.append(fish_catch) catches.append(fish_catch)
catches.sort_custom(_compare_catches) catches.sort_custom(_compare_catches)
_sorted_catches = catches _sorted_catches = catches
_inventory_empty.visible = catches.is_empty()
var visible_ids: Array[StringName] = [] var visible_ids: Array[StringName] = []
for fish_catch: FishCatchType in catches: for fish_catch: FishCatchType in catches:
visible_ids.append(fish_catch.catch_id) visible_ids.append(fish_catch.catch_id)
@ -4358,7 +4137,6 @@ func _on_catch_card_pressed(catch_id: StringName) -> void:
Input.is_key_pressed(KEY_CTRL), Input.is_key_pressed(KEY_CTRL),
Input.is_key_pressed(KEY_SHIFT) Input.is_key_pressed(KEY_SHIFT)
) )
_set_transaction_feedback("")
_refresh_inventory() _refresh_inventory()
@ -4388,7 +4166,6 @@ func _on_fish_field_gui_input(event: InputEvent) -> void:
and not get_viewport().gui_is_dragging() and not get_viewport().gui_is_dragging()
): ):
_fish_selection.clear() _fish_selection.clear()
_set_transaction_feedback("")
_refresh_inventory() _refresh_inventory()
@ -4465,8 +4242,6 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void:
_cooler_detail_texture.visible = false _cooler_detail_texture.visible = false
_cooler_detail_name.text = "select a fish" _cooler_detail_name.text = "select a fish"
_clear_cooler_detail_stats() _clear_cooler_detail_stats()
_favorite_button.disabled = true
_favorite_button.text = "favorite"
_favorite_bubble.disabled = true _favorite_bubble.disabled = true
_favorite_bubble.text = "favorite" _favorite_bubble.text = "favorite"
_favorite_bubble.persistent_mark = false _favorite_bubble.persistent_mark = false
@ -4511,12 +4286,10 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void:
else: else:
_cooler_offer_label.text = "buyer unavailable" _cooler_offer_label.text = "buyer unavailable"
_cooler_offer_value.visible = false _cooler_offer_value.visible = false
_favorite_button.disabled = false _favorite_bubble.disabled = false
_favorite_button.text = ( _favorite_bubble.text = (
"unfavorite" if fish_catch.is_favorited else "favorite" "unfavorite" if fish_catch.is_favorited else "favorite"
) )
_favorite_bubble.disabled = false
_favorite_bubble.text = _favorite_button.text
_favorite_bubble.persistent_mark = fish_catch.is_favorited _favorite_bubble.persistent_mark = fish_catch.is_favorited
_favorite_bubble.refresh_ink_state() _favorite_bubble.refresh_ink_state()
_detail_constellation.visible = _current_section == Section.COOLER _detail_constellation.visible = _current_section == Section.COOLER
@ -4555,28 +4328,19 @@ func _update_sale_summary() -> void:
active_buyer.id if active_buyer != null else StringName() active_buyer.id if active_buyer != null else StringName()
) )
_update_sell_all_action(active_buyer, buyer_id) _update_sell_all_action(active_buyer, buyer_id)
var offer_label: String = _get_offer_label(active_buyer)
var selected_ids: Array[StringName] = _fish_selection.get_selected_ids() var selected_ids: Array[StringName] = _fish_selection.get_selected_ids()
var selected_count: int = selected_ids.size() var selected_count: int = selected_ids.size()
_sell_button.text = ( _sell_bubble.text = (
"sell fish" "sell fish"
if selected_count == 1 if selected_count == 1
else "sell %d fish" % selected_count else "sell %d fish" % selected_count
) )
_sell_bubble.text = _sell_button.text
if selected_count == 0: if selected_count == 0:
_selection_summary.text = "no fish selected"
_selection_status.set_content("selected", "none")
_offer_status.set_content(offer_label, "")
_sell_button.text = "sell fish"
_sell_button.disabled = true
_sell_bubble.disabled = true _sell_bubble.disabled = true
_sell_bubble.text = "sell fish" _sell_bubble.text = "sell fish"
_sell_bubble.persistent_mark = false _sell_bubble.persistent_mark = false
_sell_bubble.refresh_ink_state() _sell_bubble.refresh_ink_state()
_set_cooler_selection_summary_empty() _set_cooler_selection_summary_empty()
_sale_unavailable.text = ""
_sale_unavailable.visible = false
return return
if ( if (
_sale_in_progress _sale_in_progress
@ -4585,51 +4349,24 @@ func _update_sale_summary() -> void:
and _network_sale_service.is_local_sale_pending() and _network_sale_service.is_local_sale_pending()
) )
): ):
_selection_summary.text = (
"1 fish selected"
if selected_count == 1
else "%d fish selected" % selected_count
)
_selection_status.set_content("selected", str(selected_count))
_offer_status.set_content(offer_label, "pending")
_sell_button.disabled = true
_sell_bubble.disabled = true _sell_bubble.disabled = true
_sale_unavailable.text = "Selling…" _set_cooler_selection_summary(selected_count, -1)
_sale_unavailable.visible = true
return return
if ( if (
_network_sale_service == null _network_sale_service == null
or buyer_id.is_empty() or buyer_id.is_empty()
or not _network_sale_service.can_request_sale(buyer_id) or not _network_sale_service.can_request_sale(buyer_id)
): ):
_selection_summary.text = (
"1 fish selected"
if selected_count == 1
else "%d fish selected" % selected_count
)
_selection_status.set_content("selected", str(selected_count))
_offer_status.set_content(offer_label, "unavailable")
_sell_button.disabled = true
_sell_bubble.disabled = true _sell_bubble.disabled = true
_sell_bubble.persistent_mark = false _sell_bubble.persistent_mark = false
_sell_bubble.refresh_ink_state() _sell_bubble.refresh_ink_state()
_sale_unavailable.text = ( _set_cooler_selection_summary(selected_count, -1)
"Selling is not supported by this server."
)
_sale_unavailable.visible = true
return return
var preview: FishSaleResultType = ( var preview: FishSaleResultType = (
_sale_service.preview_batch(selected_ids, active_buyer) _sale_service.preview_batch(selected_ids, active_buyer)
if _sale_service != null and active_buyer != null if _sale_service != null and active_buyer != null
else null else null
) )
var count_text: String = (
"1 fish selected"
if selected_count == 1
else "%d fish selected" % selected_count
)
_selection_summary.text = count_text
_selection_status.set_content("selected", str(selected_count))
if ( if (
preview != null preview != null
and preview.payout >= 0 and preview.payout >= 0
@ -4640,24 +4377,11 @@ func _update_sale_summary() -> void:
) )
): ):
_set_cooler_selection_summary(selected_count, preview.payout) _set_cooler_selection_summary(selected_count, preview.payout)
_offer_status.set_currency_amount(offer_label, preview.payout)
else: else:
_set_cooler_selection_summary(selected_count, -1) _set_cooler_selection_summary(selected_count, -1)
_offer_status.set_content(offer_label, "unavailable") _sell_bubble.disabled = preview == null or not preview.is_success()
_sell_button.disabled = preview == null or not preview.is_success()
_sell_bubble.disabled = _sell_button.disabled
_sell_bubble.persistent_mark = false _sell_bubble.persistent_mark = false
_sell_bubble.refresh_ink_state() _sell_bubble.refresh_ink_state()
if preview != null and preview.status == FishSaleResultType.Status.FAVORITED:
_sale_unavailable.text = (
"favorited fish cannot be sold. "
+ "remove them from the selection first."
)
elif preview != null and not preview.is_success():
_sale_unavailable.text = preview.get_message()
else:
_sale_unavailable.text = ""
_sale_unavailable.visible = not _sale_unavailable.text.is_empty()
func _update_sell_all_action( func _update_sell_all_action(
@ -4723,13 +4447,7 @@ func _on_favorite_pressed() -> void:
fish_catch.catch_id, fish_catch.catch_id,
not fish_catch.is_favorited not fish_catch.is_favorited
): ):
_set_transaction_feedback( _refresh_inventory()
"%s %s."
% [
fish_catch.fish.display_name,
"favorited" if fish_catch.is_favorited else "unfavorited",
]
)
func _on_sell_pressed() -> void: func _on_sell_pressed() -> void:
@ -4739,7 +4457,6 @@ func _on_sell_pressed() -> void:
func _on_sell_all_pressed() -> void: func _on_sell_all_pressed() -> void:
var catch_ids: Array[StringName] = _get_sell_all_catch_ids() var catch_ids: Array[StringName] = _get_sell_all_catch_ids()
if catch_ids.is_empty(): if catch_ids.is_empty():
_set_transaction_feedback("No sellable fish in the cooler.")
return return
_begin_sale_confirmation(catch_ids) _begin_sale_confirmation(catch_ids)
@ -4756,16 +4473,12 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void:
and _network_sale_service.is_local_sale_pending() and _network_sale_service.is_local_sale_pending()
) )
): ):
_set_transaction_feedback("Selling…")
return return
if ( if (
_network_sale_service == null _network_sale_service == null
or buyer_id.is_empty() or buyer_id.is_empty()
or not _network_sale_service.can_request_sale(buyer_id) or not _network_sale_service.can_request_sale(buyer_id)
): ):
_set_transaction_feedback(
"Selling is not supported by this server."
)
return return
if ( if (
_inventory == null _inventory == null
@ -4779,12 +4492,6 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void:
active_buyer active_buyer
) )
if not preview.is_success(): if not preview.is_success():
_set_transaction_feedback(
"favorited fish cannot be sold. "
+ "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED
else preview.get_message()
)
_refresh_inventory() _refresh_inventory()
return return
_confirmation_catch_ids = catch_ids.duplicate() _confirmation_catch_ids = catch_ids.duplicate()
@ -4847,7 +4554,7 @@ func _on_confirm_sale_pressed() -> void:
and transaction_generation == _menu_generation and transaction_generation == _menu_generation
and (visible or _shop_cooler_context_active) and (visible or _shop_cooler_context_active)
): ):
_set_transaction_feedback("Selling…") _update_sale_summary()
func _can_use_shared_world_actions() -> bool: func _can_use_shared_world_actions() -> bool:
@ -4860,16 +4567,15 @@ func _can_use_shared_world_actions() -> bool:
func _on_network_sale_pending(_request_id: String) -> void: func _on_network_sale_pending(_request_id: String) -> void:
_sale_in_progress = true _sale_in_progress = true
if visible or _shop_cooler_context_active: if visible or _shop_cooler_context_active:
_set_transaction_feedback("Selling…")
_update_sale_summary() _update_sale_summary()
func _on_network_sale_finished( func _on_network_sale_finished(
_request_id: String, _request_id: String,
accepted: bool, accepted: bool,
message: String, _message: String,
catch_ids: Array[StringName], catch_ids: Array[StringName],
payout: int, _payout: int,
) -> void: ) -> void:
_sale_in_progress = false _sale_in_progress = false
if accepted: if accepted:
@ -4877,16 +4583,6 @@ func _on_network_sale_finished(
if not visible and not _shop_cooler_context_active: if not visible and not _shop_cooler_context_active:
return return
_refresh_all() _refresh_all()
var feedback_message: String = (
"Sale complete • %s received."
% CurrencyPresentationType.bbcode_amount(payout, 20)
if accepted
else message
)
_set_transaction_feedback(feedback_message)
if not accepted:
_sale_unavailable.text = feedback_message
_sale_unavailable.visible = true
if _current_section == Section.COOLER: if _current_section == Section.COOLER:
call_deferred("_restore_inventory_tab_focus") call_deferred("_restore_inventory_tab_focus")
@ -4939,7 +4635,6 @@ func _revalidate_confirmation() -> void:
or _confirmation_buyer.id != _confirmation_buyer_id or _confirmation_buyer.id != _confirmation_buyer_id
): ):
_close_sale_confirmation() _close_sale_confirmation()
_set_transaction_feedback("sale selection is no longer available.")
return return
var preview: FishSaleResultType = _sale_service.preview_batch( var preview: FishSaleResultType = _sale_service.preview_batch(
_confirmation_catch_ids, _confirmation_catch_ids,
@ -4954,12 +4649,6 @@ func _revalidate_confirmation() -> void:
) )
): ):
_close_sale_confirmation() _close_sale_confirmation()
_set_transaction_feedback(
"favorited fish cannot be sold. "
+ "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED
else preview.get_message()
)
func _get_buyer_display_group( func _get_buyer_display_group(
@ -4974,10 +4663,6 @@ func _get_buyer_display_group(
return "buyer" return "buyer"
func _set_transaction_feedback(message: String) -> void:
_transaction_feedback.text = "[center]%s[/center]" % message
func _get_active_sale_buyer() -> FishBuyerProfileType: func _get_active_sale_buyer() -> FishBuyerProfileType:
if _sale_buyer_override != null and _sale_buyer_override.is_valid(): if _sale_buyer_override != null and _sale_buyer_override.is_valid():
return _sale_buyer_override return _sale_buyer_override
@ -4994,108 +4679,6 @@ func _get_offer_label(buyer: FishBuyerProfileType) -> String:
return "buyer offer" return "buyer offer"
func _refresh_logbook() -> void:
if not is_node_ready():
return
var valid_species: Array[FishDataType] = []
if _catalog != null:
for fish: FishDataType in _catalog.candidates:
if fish != null and fish.active and not fish.id.is_empty():
valid_species.append(fish)
_logbook_species = valid_species
_logbook_empty.visible = valid_species.is_empty()
_logbook_empty.text = (
"no fish catalog configured."
if valid_species.is_empty()
else "no species discovered yet."
)
_logbook_empty_state.visible = valid_species.is_empty()
_logbook_empty_state.text = (
"no fish catalog configured"
if valid_species.is_empty()
else ""
)
_refresh_logbook_page(false)
func _refresh_logbook_page(keep_transition_state: bool = true) -> void:
if not is_node_ready():
return
var entries_per_spread: int = 2 if _compact_layout else 4
_logbook_page_count = maxi(
1,
ceili(float(_logbook_species.size()) / float(entries_per_spread)),
)
_logbook_current_page = clampi(
_logbook_current_page,
0,
_logbook_page_count - 1,
)
_clear_logbook_entries()
var start_index: int = _logbook_current_page * entries_per_spread
var end_index: int = mini(
start_index + entries_per_spread,
_logbook_species.size(),
)
var visible_species: Array[FishDataType] = []
for index: int in range(start_index, end_index):
visible_species.append(_logbook_species[index])
if _compact_layout:
for fish: FishDataType in visible_species:
_left_entry_field.add_child(_create_logbook_entry(fish))
else:
var left_count: int = mini(2, visible_species.size())
for index: int in visible_species.size():
var destination: BoxContainer = (
_left_entry_field
if index < left_count
else _right_entry_field
)
destination.add_child(_create_logbook_entry(
visible_species[index]
))
_logbook_page_status.set_content(
"pages",
"%d / %d" % [_logbook_current_page + 1, _logbook_page_count],
)
_logbook_page_status.visible = _logbook_page_count > 1
_update_logbook_page_control_state(
keep_transition_state
and visible
and _current_section == Section.LOGBOOK
and not _transitioning
and not _page_transitioning
and not _logbook_page_transitioning
)
_configure_logbook_focus()
func _create_logbook_entry(fish: FishDataType) -> LogbookEntryType:
var discovered: bool = (
_collection_log != null
and _collection_log.has_discovered(fish.id)
)
var entry := LogbookEntryScene.instantiate() as LogbookEntryType
entry.set_meta("fish_id", fish.id)
entry.configure(
fish.display_texture,
fish.display_name,
fish.get_rarity_name(),
_inventory.get_count(fish.id) if _inventory != null else 0,
discovered,
)
entry.apply_compact_layout(_compact_layout)
return entry
func _clear_logbook_entries() -> void:
for field: BoxContainer in [_left_entry_field, _right_entry_field]:
for child: Node in field.get_children():
_release_focus_from(child, _logbook_tab)
field.remove_child(child)
child.queue_free()
func _release_focus_from(node: Node, fallback: Control) -> void: func _release_focus_from(node: Node, fallback: Control) -> void:
var focus_owner: Control = get_viewport().gui_get_focus_owner() var focus_owner: Control = get_viewport().gui_get_focus_owner()
if ( if (
@ -5119,164 +4702,6 @@ func _release_focus_from(node: Node, fallback: Control) -> void:
fallback.call_deferred("grab_focus") fallback.call_deferred("grab_focus")
func _request_logbook_page(direction: int) -> void:
if (
_current_section != Section.LOGBOOK
or _logbook_page_transitioning
or _transitioning
or _page_transitioning
):
return
var target_page: int = clampi(
_logbook_current_page + direction,
0,
_logbook_page_count - 1,
)
if target_page == _logbook_current_page:
return
_logbook_page_generation += 1
_logbook_page_transitioning = true
_update_logbook_page_control_state(false)
var generation: int = _logbook_page_generation
var travel: float = -12.0 if direction > 0 else 12.0
_logbook_page_tween = create_tween()
_logbook_page_tween.tween_property(
_book_spread,
"modulate:a",
0.0,
LOGBOOK_PAGE_DURATION * 0.5,
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_logbook_page_tween.parallel().tween_property(
_book_spread,
"position:x",
travel,
LOGBOOK_PAGE_DURATION * 0.5,
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_logbook_page_tween.tween_callback(
_swap_logbook_page.bind(target_page, travel, generation)
)
_logbook_page_tween.tween_property(
_book_spread,
"modulate:a",
1.0,
LOGBOOK_PAGE_DURATION * 0.5,
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_logbook_page_tween.parallel().tween_property(
_book_spread,
"position:x",
0.0,
LOGBOOK_PAGE_DURATION * 0.5,
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_logbook_page_tween.finished.connect(
_finish_logbook_page_transition.bind(generation),
CONNECT_ONE_SHOT,
)
func _swap_logbook_page(
target_page: int,
travel: float,
generation: int,
) -> void:
if (
generation != _logbook_page_generation
or not visible
or _current_section != Section.LOGBOOK
):
return
_logbook_current_page = target_page
_refresh_logbook_page(false)
_book_spread.position.x = -travel
_book_spread.modulate.a = 0.0
func _finish_logbook_page_transition(generation: int) -> void:
if generation != _logbook_page_generation:
return
_logbook_page_tween = null
_logbook_page_transitioning = false
_book_spread.position.x = 0.0
_book_spread.modulate.a = 1.0
_update_logbook_page_control_state(
visible and _current_section == Section.LOGBOOK
)
_configure_logbook_focus()
if _logbook_next.visible and not _logbook_next.disabled:
_logbook_next.grab_focus()
elif _logbook_previous.visible and not _logbook_previous.disabled:
_logbook_previous.grab_focus()
func _cancel_logbook_page_transition(reset_visuals: bool) -> void:
_logbook_page_generation += 1
if _logbook_page_tween != null:
_logbook_page_tween.kill()
_logbook_page_tween = null
_logbook_page_transitioning = false
if reset_visuals and is_instance_valid(_book_spread):
_book_spread.position.x = 0.0
_book_spread.modulate.a = 1.0
func _update_logbook_page_control_state(interactive: bool) -> void:
var has_multiple_pages: bool = _logbook_page_count > 1
_logbook_previous.visible = has_multiple_pages
_logbook_next.visible = has_multiple_pages
_logbook_previous.disabled = (
not interactive or _logbook_current_page <= 0
)
_logbook_next.disabled = (
not interactive
or _logbook_current_page >= _logbook_page_count - 1
)
for control: BubbleButtonType in [_logbook_previous, _logbook_next]:
control.focus_mode = (
Control.FOCUS_ALL
if control.visible and not control.disabled
else Control.FOCUS_NONE
)
control.mouse_filter = (
Control.MOUSE_FILTER_STOP
if control.visible and not control.disabled
else Control.MOUSE_FILTER_IGNORE
)
func _configure_logbook_focus() -> void:
if _current_section != Section.LOGBOOK:
return
_logbook_tab.focus_neighbor_bottom = _logbook_tab.focus_neighbor_right
if _logbook_page_count <= 1:
return
var first_control: BubbleButtonType = (
_logbook_previous
if not _logbook_previous.disabled
else _logbook_next
)
_logbook_tab.focus_neighbor_bottom = _logbook_tab.get_path_to(
first_control
)
for control: BubbleButtonType in [_logbook_previous, _logbook_next]:
control.focus_neighbor_top = control.get_path_to(_logbook_tab)
control.focus_neighbor_bottom = control.focus_neighbor_top
control.focus_neighbor_left = control.get_path_to(
_logbook_previous
)
control.focus_neighbor_right = control.get_path_to(_logbook_next)
func _advance_logbook_page_controls(delta: float) -> void:
for control: BubbleButtonType in [_logbook_previous, _logbook_next]:
if not control.visible:
continue
control.advance_emphasis(delta)
control.apply_presentation(
control.calculate_target(_motion_elapsed, 1.0, 1.0),
control.calculate_visual_scale(_motion_elapsed, 1.0),
minf(1.0, delta * 10.0),
)
func _create_texture_frame( func _create_texture_frame(
texture: Texture2D, texture: Texture2D,
minimum_size: Vector2, minimum_size: Vector2,

View file

@ -1,13 +1,10 @@
[gd_scene load_steps=34 format=3] [gd_scene load_steps=30 format=3]
[ext_resource type="Script" path="res://ui/player_menu.gd" id="1_menu"] [ext_resource type="Script" path="res://ui/player_menu.gd" id="1_menu"]
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_content_shell.gd" id="3_shell"]
[ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="4_profile"] [ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="4_profile"]
[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_cluster.gd" id="5_cluster"] [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_cluster.gd" id="5_cluster"]
[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_button.gd" id="6_bubble"] [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_button.gd" id="6_bubble"]
[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_status_bubble.tscn" id="7_status"]
[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_information_panel.gd" id="8_info"]
[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_information_panel.tscn" id="9_info_scene"] [ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_information_panel.tscn" id="9_info_scene"]
[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_status_bubble.tscn" id="10_status_scene"] [ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_status_bubble.tscn" id="10_status_scene"]
[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_button.gd" id="11_bubble"] [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_button.gd" id="11_bubble"]
@ -33,8 +30,6 @@
[ext_resource type="Texture2D" path="res://ui/icons/tab_menu/close.png" id="31_close"] [ext_resource type="Texture2D" path="res://ui/icons/tab_menu/close.png" id="31_close"]
[ext_resource type="PackedScene" path="res://ui/components/currency_amount.tscn" id="32_currency"] [ext_resource type="PackedScene" path="res://ui/components/currency_amount.tscn" id="32_currency"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_collection"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_cooler_water"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_cooler_water"]
shader = ExtResource("14_water") shader = ExtResource("14_water")
shader_parameter/decorative_bubble_1 = ExtResource("15_bubble1") shader_parameter/decorative_bubble_1 = ExtResource("15_bubble1")
@ -814,597 +809,6 @@ offset_bottom = 720.0
mouse_filter = 1 mouse_filter = 1
script = ExtResource("20_players_page") script = ExtResource("20_players_page")
[node name="BookBacking" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage"]
unique_name_in_owner = true
visible = false
layout_mode = 0
offset_left = 58.0
offset_top = 128.0
offset_right = 1222.0
offset_bottom = 588.0
mouse_filter = 2
[node name="BookOuterMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking"]
layout_mode = 2
mouse_filter = 2
theme_override_constants/margin_left = 12
theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 12
theme_override_constants/margin_bottom = 12
[node name="BookSpread" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin"]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 2
theme_override_constants/separation = 0
[node name="LeftPage" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="LeftPageMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/LeftPage"]
layout_mode = 2
mouse_filter = 2
theme_override_constants/margin_left = 22
theme_override_constants/margin_top = 18
theme_override_constants/margin_right = 24
theme_override_constants/margin_bottom = 18
[node name="LeftPageLayout" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/LeftPage/LeftPageMargin"]
layout_mode = 2
mouse_filter = 2
theme_override_constants/separation = 8
[node name="LeftHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/LeftPage/LeftPageMargin/LeftPageLayout"]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 2
theme_override_colors/font_color = Color(0.22, 0.16, 0.09, 1)
theme_override_font_sizes/font_size = 20
text = "field notes"
horizontal_alignment = 1
[node name="LeftEntryField" type="BoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/LeftPage/LeftPageMargin/LeftPageLayout"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
mouse_filter = 2
theme_override_constants/separation = 10
vertical = true
[node name="BookGutter" type="ColorRect" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread"]
unique_name_in_owner = true
custom_minimum_size = Vector2(18, 0)
layout_mode = 2
mouse_filter = 2
color = Color(0.44, 0.34, 0.21, 0.52)
[node name="RightPage" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="RightPageMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/RightPage"]
layout_mode = 2
mouse_filter = 2
theme_override_constants/margin_left = 24
theme_override_constants/margin_top = 18
theme_override_constants/margin_right = 22
theme_override_constants/margin_bottom = 18
[node name="RightPageLayout" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/RightPage/RightPageMargin"]
layout_mode = 2
mouse_filter = 2
theme_override_constants/separation = 8
[node name="RightHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/RightPage/RightPageMargin/RightPageLayout"]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 2
theme_override_colors/font_color = Color(0.22, 0.16, 0.09, 1)
theme_override_font_sizes/font_size = 20
text = "catch record"
horizontal_alignment = 1
[node name="RightEntryField" type="BoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/RightPage/RightPageMargin/RightPageLayout"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
mouse_filter = 2
theme_override_constants/separation = 10
vertical = true
[node name="LogbookEmptyState" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage"]
unique_name_in_owner = true
visible = false
layout_mode = 0
offset_left = 402.0
offset_top = 314.0
offset_right = 878.0
offset_bottom = 366.0
mouse_filter = 2
theme_override_colors/font_color = Color(0.28, 0.21, 0.13, 0.82)
theme_override_font_sizes/font_size = 20
text = "no fish catalog configured"
horizontal_alignment = 1
vertical_alignment = 1
[node name="LogbookPrevious" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(76, 72)
layout_mode = 0
offset_left = 78.0
offset_top = 494.0
offset_right = 154.0
offset_bottom = 566.0
text = "previous"
script = ExtResource("11_bubble")
profile = ExtResource("4_profile")
neutral_size = Vector2(76, 72)
desktop_anchor = Vector2(116, 530)
compact_anchor = Vector2(58, 332)
compact_minimum_size = Vector2(66, 62)
minimum_font_size = 11
maximum_font_size = 15
horizontal_amplitude = 0.0
vertical_amplitude = 0.5
deformation_amplitude = 0.006
[node name="LogbookNext" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(76, 72)
layout_mode = 0
offset_left = 1126.0
offset_top = 494.0
offset_right = 1202.0
offset_bottom = 566.0
text = "next"
script = ExtResource("11_bubble")
profile = ExtResource("4_profile")
neutral_size = Vector2(76, 72)
desktop_anchor = Vector2(1164, 530)
compact_anchor = Vector2(582, 332)
compact_minimum_size = Vector2(66, 62)
minimum_font_size = 11
maximum_font_size = 15
horizontal_amplitude = 0.0
vertical_amplitude = 0.5
motion_phase = 2.2
deformation_amplitude = 0.006
[node name="LogbookPageStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage" instance=ExtResource("10_status_scene")]
unique_name_in_owner = true
visible = false
layout_mode = 0
offset_left = 555.0
offset_top = 524.0
offset_right = 725.0
offset_bottom = 580.0
mouse_filter = 2
[node name="MenuPanel" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
unique_name_in_owner = true
layout_mode = 0
offset_left = 42.0
offset_top = 104.0
offset_right = 1238.0
offset_bottom = 582.0
script = ExtResource("3_shell")
profile = ExtResource("4_profile")
content_margin = 22.0
corner_radius = 74
[node name="Margin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel"]
layout_mode = 2
theme_override_constants/margin_left = 14
theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 14
theme_override_constants/margin_bottom = 12
[node name="Layout" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin"]
layout_mode = 2
theme_override_constants/separation = 8
[node name="Header" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 8
[node name="WalletHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Header"]
layout_mode = 2
text = "wallet"
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
[node name="WalletBalance" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Header" instance=ExtResource("32_currency")]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
alignment = 0
[node name="Separator" type="HSeparator" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"]
unique_name_in_owner = true
layout_mode = 2
[node name="TransactionFeedback" type="RichTextLabel" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 30)
layout_mode = 2
text = ""
bbcode_enabled = true
fit_content = true
scroll_active = false
theme_override_colors/default_color = Color(1, 0.82, 0.4, 1)
theme_override_font_sizes/normal_font_size = 21
[node name="Content" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 260)
layout_mode = 2
size_flags_vertical = 3
[node name="InventorySection" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 7
[node name="StatusShoal" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 8
alignment = 1
[node name="WalletStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/StatusShoal" instance=ExtResource("7_status")]
unique_name_in_owner = true
layout_mode = 2
[node name="CapacityStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/StatusShoal" instance=ExtResource("7_status")]
unique_name_in_owner = true
layout_mode = 2
[node name="HeldValueStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/StatusShoal" instance=ExtResource("7_status")]
unique_name_in_owner = true
layout_mode = 2
[node name="SortBar" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 6
[node name="SortLabel" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
layout_mode = 2
text = "sort:"
[node name="SortOption" type="OptionButton" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(136, 104)
[node name="SortDirection" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(136, 104)
text = "newest first"
script = ExtResource("6_bubble")
profile = ExtResource("4_profile")
neutral_size = Vector2(136, 104)
minimum_font_size = 15
maximum_font_size = 20
horizontal_amplitude = 0.0
vertical_amplitude = 0.0
deformation_amplitude = 0.0
[node name="SortSpacer" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="CoolerCount" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
unique_name_in_owner = true
visible = false
layout_mode = 2
text = "0 / 12"
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1)
[node name="HeldValueHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
visible = false
layout_mode = 2
text = "held fish base value"
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
[node name="HeldValue" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar" instance=ExtResource("32_currency")]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="InventoryBody" type="BoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/separation = 8
[node name="InventoryList" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody"]
unique_name_in_owner = true
custom_minimum_size = Vector2(350, 0)
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxEmpty_collection")
[node name="InventoryListMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList"]
layout_mode = 2
theme_override_constants/margin_left = 8
theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 8
theme_override_constants/margin_bottom = 8
[node name="InventoryStack" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin"]
layout_mode = 2
theme_override_constants/separation = 6
[node name="InventoryEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"]
unique_name_in_owner = true
layout_mode = 2
text = "your cooler is empty."
horizontal_alignment = 1
[node name="InventoryScroll" type="ScrollContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="InventoryGrid" type="GridContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack/InventoryScroll"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 7
theme_override_constants/v_separation = 7
columns = 3
[node name="DetailPanel" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody"]
unique_name_in_owner = true
custom_minimum_size = Vector2(210, 0)
layout_mode = 2
script = ExtResource("8_info")
profile = ExtResource("4_profile")
content_margin = 12.0
corner_radius = 96
[node name="DetailMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel"]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 9
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 9
[node name="DetailStack" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin"]
layout_mode = 2
theme_override_constants/separation = 6
[node name="DetailTexture" type="TextureRect" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
unique_name_in_owner = true
custom_minimum_size = Vector2(178, 100)
layout_mode = 2
expand_mode = 1
stretch_mode = 5
texture_filter = 1
[node name="DetailName" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1)
theme_override_font_sizes/font_size = 29
horizontal_alignment = 1
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
[node name="DetailData" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
unique_name_in_owner = true
layout_mode = 2
autowrap_mode = 2
horizontal_alignment = 1
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
[node name="SelectionSummary" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
unique_name_in_owner = true
visible = false
layout_mode = 2
text = "no fish selected"
autowrap_mode = 2
horizontal_alignment = 1
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1)
[node name="ActionRow" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
layout_mode = 2
theme_override_constants/separation = 8
alignment = 1
[node name="FavoriteButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack/ActionRow"]
unique_name_in_owner = true
layout_mode = 2
disabled = true
text = "favorite"
custom_minimum_size = Vector2(112, 104)
size_flags_horizontal = 4
script = ExtResource("6_bubble")
profile = ExtResource("4_profile")
neutral_size = Vector2(112, 104)
minimum_font_size = 15
maximum_font_size = 20
horizontal_amplitude = 0.0
vertical_amplitude = 0.0
deformation_amplitude = 0.0
[node name="SellButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack/ActionRow"]
unique_name_in_owner = true
layout_mode = 2
disabled = true
text = "sell"
custom_minimum_size = Vector2(112, 104)
size_flags_horizontal = 4
script = ExtResource("6_bubble")
profile = ExtResource("4_profile")
neutral_size = Vector2(112, 104)
minimum_font_size = 15
maximum_font_size = 20
horizontal_amplitude = 0.0
vertical_amplitude = 0.0
deformation_amplitude = 0.0
[node name="ContextStatus" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
layout_mode = 2
theme_override_constants/separation = 6
alignment = 1
[node name="SelectionStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack/ContextStatus" instance=ExtResource("7_status")]
unique_name_in_owner = true
custom_minimum_size = Vector2(108, 78)
layout_mode = 2
[node name="OfferStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack/ContextStatus" instance=ExtResource("7_status")]
unique_name_in_owner = true
custom_minimum_size = Vector2(128, 78)
layout_mode = 2
[node name="SaleUnavailable" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
unique_name_in_owner = true
visible = false
layout_mode = 2
autowrap_mode = 2
horizontal_alignment = 1
theme_override_colors/font_color = Color(1, 0.702, 0.278, 1)
[node name="BagSection" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content"]
unique_name_in_owner = true
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 7
[node name="BagHint" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection"]
layout_mode = 2
text = "drag fish or hotbar-compatible items onto a slot below. right-click a slot to clear it."
horizontal_alignment = 1
autowrap_mode = 2
[node name="BagBody" type="HSplitContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection"]
layout_mode = 2
size_flags_vertical = 3
split_offset = 540
theme_override_constants/separation = 8
[node name="BagList" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody"]
unique_name_in_owner = true
custom_minimum_size = Vector2(360, 0)
layout_mode = 2
[node name="Margin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList"]
layout_mode = 2
theme_override_constants/margin_left = 8
theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 8
theme_override_constants/margin_bottom = 8
[node name="Stack" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin"]
layout_mode = 2
theme_override_constants/separation = 6
[node name="BagEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack"]
unique_name_in_owner = true
layout_mode = 2
text = "your bag is empty."
horizontal_alignment = 1
[node name="BagScroll" type="ScrollContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack"]
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="BagGrid" type="GridContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack/BagScroll"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 7
theme_override_constants/v_separation = 7
columns = 3
[node name="BagDetail" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody"]
unique_name_in_owner = true
custom_minimum_size = Vector2(220, 0)
layout_mode = 2
[node name="Margin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail"]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 9
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 9
[node name="Stack" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin"]
layout_mode = 2
theme_override_constants/separation = 6
[node name="BagDetailTexture" type="TextureRect" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin/Stack"]
unique_name_in_owner = true
custom_minimum_size = Vector2(180, 96)
layout_mode = 2
expand_mode = 1
stretch_mode = 5
texture_filter = 1
[node name="BagDetailName" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin/Stack"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 29
horizontal_alignment = 1
[node name="BagDetailData" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin/Stack"]
unique_name_in_owner = true
layout_mode = 2
text = "select a bag item for details."
horizontal_alignment = 1
autowrap_mode = 2
[node name="LogbookSection" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content"]
unique_name_in_owner = true
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 7
[node name="LogbookEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/LogbookSection"]
unique_name_in_owner = true
layout_mode = 2
text = "no species discovered yet."
horizontal_alignment = 1
[node name="LogbookScroll" type="ScrollContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/LogbookSection"]
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="LogbookGrid" type="GridContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/LogbookSection/LogbookScroll"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 7
theme_override_constants/v_separation = 7
columns = 4
[node name="SaleConfirmation" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"] [node name="SaleConfirmation" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false

View file

@ -219,7 +219,12 @@ func consume_escape() -> bool:
func handle_controller_input(event: InputEvent) -> bool: func handle_controller_input(event: InputEvent) -> bool:
if not _profile_active or not _profile_interactive: if not _profile_active or not _profile_interactive:
return false return false
if event is InputEventJoypadButton or event is InputEventJoypadMotion: if (
event.is_action_pressed(&"ui_up")
or event.is_action_pressed(&"ui_down")
or event.is_action_pressed(&"ui_left")
or event.is_action_pressed(&"ui_right")
):
_ensure_controller_zone_focus() _ensure_controller_zone_focus()
var cancel_pressed := _event_matches_controller_press( var cancel_pressed := _event_matches_controller_press(
event, event,

View file

@ -1,20 +1,8 @@
class_name QuickRadialMenu class_name QuickRadialMenu
extends Control extends RadialActionMenu
signal action_selected(action_id: StringName) signal action_selected(action_id: StringName)
const BubbleButtonScene: PackedScene = preload(
"res://ui/components/bubble_menu/bubble_button.tscn"
)
const BubbleProfile: BubbleMenuProfile = preload(
"res://ui/components/bubble_menu/bubble_menu_profile.tres"
)
const RING_RADIUS: float = 172.0
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const ACTIONS: Array[StringName] = [ const ACTIONS: Array[StringName] = [
&"chat", &"chat",
&"freecam", &"freecam",
@ -27,80 +15,20 @@ const LABELS: Array[String] = [
] ]
const SECTOR_COUNT: int = 3 const SECTOR_COUNT: int = 3
var _buttons: Array[BubbleButton] = []
var _is_open: bool = false
var _selected_sector: int = 0
var _controller_selection_mode: bool = false
var _controller_mapping_manager: ControllerMappingManagerType
var _hud_hidden: bool = false var _hud_hidden: bool = false
func setup_controller_mapping(
mapping_manager: ControllerMappingManagerType,
) -> void:
_controller_mapping_manager = mapping_manager
func _ready() -> void:
visible = false
mouse_filter = Control.MOUSE_FILTER_IGNORE
for sector: int in SECTOR_COUNT:
var bubble: BubbleButton = BubbleButtonScene.instantiate() as BubbleButton
bubble.profile = BubbleProfile
bubble.focus_mode = Control.FOCUS_NONE
bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
bubble.text = _label_for_sector(sector)
add_child(bubble)
_buttons.append(bubble)
_apply_selection_styles()
func handle_input(event: InputEvent, can_open: bool) -> bool:
if not event.is_action("open_quick_actions"):
return false
var key_event: InputEventKey = event as InputEventKey
if key_event != null and key_event.echo:
return false
if event.is_pressed():
if _is_open or not can_open:
return _is_open
open_menu(event is InputEventJoypadButton)
return true
if not _is_open:
return false
var selected: int = _selected_sector
close_menu()
call_deferred("_emit_selected_action", ACTIONS[selected])
return true
func open_menu(controller_selection: bool = false) -> void:
_is_open = true
_selected_sector = 0
_controller_selection_mode = controller_selection
visible = true
_refresh_labels()
_layout_bubbles()
_apply_selection_styles()
func close_menu() -> void:
_is_open = false
visible = false
func is_open() -> bool:
return _is_open
func set_hud_hidden(hidden: bool) -> void: func set_hud_hidden(hidden: bool) -> void:
_hud_hidden = hidden _hud_hidden = hidden
_refresh_labels() _refresh_labels()
func _refresh_labels() -> void: func _input_action_name() -> StringName:
for sector: int in _buttons.size(): return &"open_quick_actions"
_buttons[sector].text = _label_for_sector(sector)
func _sector_count() -> int:
return SECTOR_COUNT
func _label_for_sector(sector: int) -> String: func _label_for_sector(sector: int) -> String:
@ -109,76 +37,9 @@ func _label_for_sector(sector: int) -> String:
return LABELS[sector] return LABELS[sector]
func _emit_selected_sector(sector: int) -> void:
call_deferred("_emit_selected_action", ACTIONS[sector])
func _emit_selected_action(action_id: StringName) -> void: func _emit_selected_action(action_id: StringName) -> void:
action_selected.emit(action_id) action_selected.emit(action_id)
func _process(_delta: float) -> void:
if not _is_open:
return
_layout_bubbles()
_update_selection()
func _layout_bubbles() -> void:
var center: Vector2 = size * 0.5
for sector: int in SECTOR_COUNT:
var angle: float = -PI * 0.5 + TAU * float(sector) / float(SECTOR_COUNT)
var bubble_center: Vector2 = center + Vector2.from_angle(angle) * RING_RADIUS
var bubble: BubbleButton = _buttons[sector]
bubble.position = bubble_center - BUBBLE_SIZE * 0.5
bubble.size = BUBBLE_SIZE
bubble.pivot_offset = BUBBLE_SIZE * 0.5
func _update_selection() -> void:
var stick: Vector2 = _get_selection_stick()
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
_select_sector_from_offset(stick)
return
if _controller_selection_mode:
return
_update_mouse_selection()
func _get_selection_stick() -> Vector2:
if _controller_mapping_manager != null:
return Vector2(
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
),
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
),
)
return Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
)
func _update_mouse_selection() -> void:
var offset: Vector2 = get_local_mouse_position() - size * 0.5
if offset.length() < 24.0:
return
_select_sector_from_offset(offset)
func _select_sector_from_offset(offset: Vector2) -> void:
var half_sector: float = TAU / (float(SECTOR_COUNT) * 2.0)
var angle: float = fposmod(offset.angle() + PI * 0.5 + half_sector, TAU)
var sector: int = int(floor(angle / (TAU / float(SECTOR_COUNT))))
if sector == _selected_sector:
return
_selected_sector = sector
_apply_selection_styles()
func _apply_selection_styles() -> void:
for sector: int in _buttons.size():
var bubble: BubbleButton = _buttons[sector]
bubble.apply_profile()
if sector == _selected_sector:
bubble.add_theme_stylebox_override(
"normal", BubbleProfile.make_hover_style()
)

176
ui/radial_action_menu.gd Normal file
View file

@ -0,0 +1,176 @@
class_name RadialActionMenu
extends Control
const BubbleButtonScene: PackedScene = preload(
"res://ui/components/bubble_menu/bubble_button.tscn"
)
const BubbleProfile: BubbleMenuProfile = preload(
"res://ui/components/bubble_menu/bubble_menu_profile.tres"
)
const RING_RADIUS: float = 172.0
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
var _buttons: Array[BubbleButton] = []
var _is_open: bool = false
var _selected_sector: int = 0
var _controller_selection_mode: bool = false
var _controller_mapping_manager: ControllerMappingManagerType
func setup_controller_mapping(
mapping_manager: ControllerMappingManagerType,
) -> void:
_controller_mapping_manager = mapping_manager
func _ready() -> void:
visible = false
mouse_filter = Control.MOUSE_FILTER_IGNORE
for sector: int in _sector_count():
var bubble := BubbleButtonScene.instantiate() as BubbleButton
bubble.profile = BubbleProfile
bubble.focus_mode = Control.FOCUS_NONE
bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
bubble.text = _label_for_sector(sector)
add_child(bubble)
_buttons.append(bubble)
_apply_selection_styles()
func handle_input(event: InputEvent, can_open: bool) -> bool:
if not event.is_action(_input_action_name()):
return false
var key_event := event as InputEventKey
if key_event != null and key_event.echo:
return false
if event.is_pressed():
if _is_open or not can_open:
return _is_open
open_menu(event is InputEventJoypadButton)
return true
if not _is_open:
return false
var selected: int = _selected_sector
close_menu()
_emit_selected_sector(selected)
return true
func open_menu(controller_selection: bool = false) -> void:
_is_open = true
_selected_sector = _initial_sector()
_controller_selection_mode = controller_selection
visible = true
_refresh_labels()
_layout_bubbles()
_apply_selection_styles()
func close_menu() -> void:
_is_open = false
visible = false
func is_open() -> bool:
return _is_open
func _refresh_labels() -> void:
for sector: int in _buttons.size():
_buttons[sector].text = _label_for_sector(sector)
func _process(_delta: float) -> void:
if not _is_open:
return
_layout_bubbles()
_update_selection()
func _layout_bubbles() -> void:
var center: Vector2 = size * 0.5
var sector_count: int = _sector_count()
for sector: int in sector_count:
var angle: float = (
-PI * 0.5 + TAU * float(sector) / float(sector_count)
)
var bubble_center: Vector2 = (
center + Vector2.from_angle(angle) * RING_RADIUS
)
var bubble: BubbleButton = _buttons[sector]
bubble.position = bubble_center - BUBBLE_SIZE * 0.5
bubble.size = BUBBLE_SIZE
bubble.pivot_offset = BUBBLE_SIZE * 0.5
func _update_selection() -> void:
var stick: Vector2 = _get_selection_stick()
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
_select_sector_from_offset(stick)
return
if _controller_selection_mode:
return
var offset: Vector2 = get_local_mouse_position() - size * 0.5
if offset.length() >= 24.0:
_select_sector_from_offset(offset)
func _get_selection_stick() -> Vector2:
if _controller_mapping_manager != null:
return Vector2(
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
),
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
),
)
return Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
)
func _select_sector_from_offset(offset: Vector2) -> void:
var sector_count: int = _sector_count()
var half_sector: float = TAU / (float(sector_count) * 2.0)
var angle: float = fposmod(offset.angle() + PI * 0.5 + half_sector, TAU)
var sector: int = int(floor(angle / (TAU / float(sector_count))))
if sector == _selected_sector:
return
_selected_sector = sector
_apply_selection_styles()
func _apply_selection_styles() -> void:
for sector: int in _buttons.size():
var bubble: BubbleButton = _buttons[sector]
bubble.apply_profile()
if sector == _selected_sector:
bubble.add_theme_stylebox_override(
"normal", BubbleProfile.make_hover_style()
)
func _input_action_name() -> StringName:
return StringName()
func _sector_count() -> int:
return 1
func _initial_sector() -> int:
return 0
func _label_for_sector(_sector: int) -> String:
return ""
func _emit_selected_sector(_sector: int) -> void:
pass

View file

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

View file

@ -13,9 +13,6 @@ const ControllerFocusNavigationType = preload(
@export var back_focus_path: NodePath @export var back_focus_path: NodePath
@export var back_entry_path: NodePath @export var back_entry_path: NodePath
@export var maximum_layout_size: Vector2 = Vector2(720.0, 520.0) @export var maximum_layout_size: Vector2 = Vector2(720.0, 520.0)
@export var compact_maximum_layout_size: Vector2 = Vector2.ZERO
@export var compact_width_threshold: float = 680.0
@export var compact_height_threshold: float = 500.0
@export_range(0.0, 128.0, 1.0) var transition_safe_margin: float = 24.0 @export_range(0.0, 128.0, 1.0) var transition_safe_margin: float = 24.0
var _cluster: BubbleCluster var _cluster: BubbleCluster
@ -176,13 +173,9 @@ func _process(delta: float) -> void:
func _update_layout() -> void: func _update_layout() -> void:
if not is_node_ready() or _cluster == null: if not is_node_ready() or _cluster == null:
return return
var compact: bool = false
var layout_maximum: Vector2 = maximum_layout_size
if compact and compact_maximum_layout_size != Vector2.ZERO:
layout_maximum = compact_maximum_layout_size
var field_size := Vector2( var field_size := Vector2(
minf(size.x, layout_maximum.x), minf(size.x, maximum_layout_size.x),
minf(size.y, layout_maximum.y) minf(size.y, maximum_layout_size.y)
) )
field_size.x = maxf(1.0, field_size.x) field_size.x = maxf(1.0, field_size.x)
field_size.y = maxf(1.0, field_size.y) field_size.y = maxf(1.0, field_size.y)
@ -190,7 +183,7 @@ func _update_layout() -> void:
if not _is_transitioning: if not _is_transitioning:
_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, false)
_configure_focus_navigation() _configure_focus_navigation()

View file

@ -33,10 +33,6 @@ const KeyboardMouseMappingPanelType = preload(
const DialogControllerNavigationType = preload( const DialogControllerNavigationType = preload(
"res://ui/file_dialog_controller_navigation.gd" "res://ui/file_dialog_controller_navigation.gd"
) )
const ControllerFocusNavigationType = preload(
"res://ui/controller_focus_navigation.gd"
)
signal applied signal applied
signal closed signal closed
signal closing signal closing
@ -117,6 +113,10 @@ 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 _on_screen_keyboard_enabled: bool = false
var _chat_dock_right: bool = false
var _chat_mobile_mode: bool = false
var _paint_dock_right: bool = true
var _fullscreen_enabled: bool = false
var _master_volume: float = 1.0 var _master_volume: float = 1.0
var _music_volume: float = 1.0 var _music_volume: float = 1.0
var _effects_volume: float = 1.0 var _effects_volume: float = 1.0
@ -144,6 +144,11 @@ var _pending_identity_path := ""
var _pending_import_data: Dictionary = {} var _pending_import_data: Dictionary = {}
func _notification(what: int) -> void:
if what == NOTIFICATION_VISIBILITY_CHANGED and not visible:
_release_owned_focus()
func _ready() -> void: func _ready() -> void:
_pages = { _pages = {
PAGE_DISPLAY: _display_page, PAGE_DISPLAY: _display_page,
@ -235,7 +240,7 @@ func _connect_controls() -> void:
) )
%ControllerMapping.pressed.connect(_open_controller_mapping) %ControllerMapping.pressed.connect(_open_controller_mapping)
%KeyboardMapping.pressed.connect(_open_keyboard_mouse_mapping) %KeyboardMapping.pressed.connect(_open_keyboard_mouse_mapping)
%OpenDataFolder.pressed.connect(_confirm_open_data_folder) %OpenDataFolder.pressed.connect(_open_data_folder)
%ChangeDataFolder.pressed.connect(_choose_data_folder) %ChangeDataFolder.pressed.connect(_choose_data_folder)
%ExportPlayerIdentity.pressed.connect( %ExportPlayerIdentity.pressed.connect(
_choose_identity_export.bind("player") _choose_identity_export.bind("player")
@ -348,6 +353,7 @@ func open_panel(
animate_gameplay_host_transitions: bool = false, animate_gameplay_host_transitions: bool = false,
) -> void: ) -> void:
_cancel_panel_transition() _cancel_panel_transition()
_release_owned_focus()
_presentation_mode = presentation_mode _presentation_mode = presentation_mode
_animate_gameplay_host_transitions = animate_gameplay_host_transitions _animate_gameplay_host_transitions = animate_gameplay_host_transitions
_settings_manager = settings_manager _settings_manager = settings_manager
@ -359,7 +365,6 @@ func open_panel(
panel_visibility_changed.emit(true) panel_visibility_changed.emit(true)
_main_panel.modulate.a = 1.0 _main_panel.modulate.a = 1.0
_main_panel.scale = Vector2.ONE _main_panel.scale = Vector2.ONE
call_deferred("_configure_controller_navigation")
call_deferred("_focus_active_tab") call_deferred("_focus_active_tab")
if ( if (
presentation_mode == PresentationMode.TITLE_EMBEDDED presentation_mode == PresentationMode.TITLE_EMBEDDED
@ -472,6 +477,7 @@ func _finish_panel_close(applied_result: bool) -> void:
_keyboard_mouse_mapping_panel.close_panel() _keyboard_mouse_mapping_panel.close_panel()
_main_panel.modulate.a = 1.0 _main_panel.modulate.a = 1.0
_main_panel.scale = Vector2.ONE _main_panel.scale = Vector2.ONE
_release_owned_focus()
hide() hide()
panel_visibility_changed.emit(false) panel_visibility_changed.emit(false)
if applied_result: if applied_result:
@ -521,6 +527,7 @@ func handle_back() -> void:
func _select_page(page_id: StringName, focus_tab: bool = true) -> void: func _select_page(page_id: StringName, focus_tab: bool = true) -> void:
if not _pages.has(page_id): if not _pages.has(page_id):
return return
var target_tab: OrganizerTab = _tabs.get(page_id)
_active_page_id = page_id _active_page_id = page_id
for candidate_id: StringName in _pages: for candidate_id: StringName in _pages:
_pages[candidate_id].visible = candidate_id == page_id _pages[candidate_id].visible = candidate_id == page_id
@ -530,9 +537,8 @@ func _select_page(page_id: StringName, focus_tab: bool = true) -> void:
) )
if page_id == PAGE_DATA: if page_id == PAGE_DATA:
_refresh_data_page() _refresh_data_page()
call_deferred("_configure_controller_navigation") if focus_tab and target_tab != null:
if focus_tab: target_tab.grab_focus()
_tabs[page_id].call_deferred("grab_focus")
func _focus_active_tab() -> void: func _focus_active_tab() -> void:
@ -541,30 +547,6 @@ func _focus_active_tab() -> void:
tab.grab_focus() tab.grab_focus()
func _configure_controller_navigation() -> void:
if not is_node_ready() or not visible:
return
var controls: Array[Control] = []
_collect_focusable_controls(_main_panel, controls)
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
func _collect_focusable_controls(
node: Node,
controls: Array[Control],
) -> void:
for child: Node in node.get_children():
var control := child as Control
if control != null and not control.is_visible_in_tree():
continue
if (
control != null
and ControllerFocusNavigationType.is_focusable(control)
):
controls.append(control)
_collect_focusable_controls(child, controls)
func _open_controller_mapping() -> void: func _open_controller_mapping() -> void:
if _controller_mapping_manager == null: if _controller_mapping_manager == null:
_feedback.text = "no controller mapping service is available" _feedback.text = "no controller mapping service is available"
@ -627,38 +609,30 @@ func _refresh_data_page() -> void:
"copy player fingerprint · %s" "copy player fingerprint · %s"
% NetworkIdentityCrypto.compact_suffix(fingerprint) % NetworkIdentityCrypto.compact_suffix(fingerprint)
) )
call_deferred("_configure_controller_navigation")
func _open_data_folder() -> void: func _open_data_folder() -> void:
if _data_root == null or not _data_root.open_folder(): if (
_feedback.text = "could not open the data folder." not is_visible_in_tree()
or _active_page_id != PAGE_DATA
or not _data_page.is_visible_in_tree()
func _confirm_open_data_folder() -> void: or not %OpenDataFolder.is_visible_in_tree()
or get_viewport().gui_get_focus_owner() != %OpenDataFolder
):
return
if _data_root == null: if _data_root == null:
_feedback.text = "the data folder is unavailable." _feedback.text = "the data folder is unavailable."
return return
var dialog := ConfirmationDialog.new() if not _data_root.open_folder():
dialog.title = "open data folder?" _feedback.text = "could not open the data folder."
dialog.ok_button_text = "open folder"
dialog.cancel_button_text = "cancel"
dialog.dialog_text = ( func _release_owned_focus() -> void:
"open this folder outside NETfishing?\n\n" + _data_root.root_path if not is_inside_tree():
) return
dialog.confirmed.connect(func() -> void: var focus_owner: Control = get_viewport().gui_get_focus_owner()
_open_data_folder() if focus_owner != null and is_ancestor_of(focus_owner):
dialog.queue_free() focus_owner.release_focus()
)
dialog.canceled.connect(dialog.queue_free)
if _interface_fonts != null:
_interface_fonts.apply_utility_theme(dialog)
add_child(dialog)
dialog.popup_centered(Vector2i(620, 300))
_configure_confirmation_dialog.call_deferred(
dialog,
dialog.get_cancel_button(),
)
func _copy_player_fingerprint() -> void: func _copy_player_fingerprint() -> void:
@ -950,6 +924,10 @@ func _apply_settings() -> void:
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 edited.on_screen_keyboard_enabled = _on_screen_keyboard_enabled
edited.chat_dock_right = _chat_dock_right
edited.chat_mobile_mode = _chat_mobile_mode
edited.paint_dock_right = _paint_dock_right
edited.fullscreen_enabled = _fullscreen_enabled
edited.master_volume = _master_volume edited.master_volume = _master_volume
edited.music_volume = _music_volume edited.music_volume = _music_volume
edited.effects_volume = _effects_volume edited.effects_volume = _effects_volume
@ -976,16 +954,20 @@ func _load_controls() -> void:
_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 _on_screen_keyboard_enabled = settings.on_screen_keyboard_enabled
_chat_dock_right = settings.chat_dock_right
_chat_mobile_mode = settings.chat_mobile_mode
_paint_dock_right = settings.paint_dock_right
_fullscreen_enabled = settings.fullscreen_enabled
_master_volume = settings.master_volume _master_volume = settings.master_volume
_music_volume = settings.music_volume _music_volume = settings.music_volume
_effects_volume = settings.effects_volume _effects_volume = settings.effects_volume
_environment_volume = settings.environment_volume _environment_volume = settings.environment_volume
_world_pixelation.select(settings.world_pixel_size - 1) _world_pixelation.select(settings.world_pixel_size - 1)
_ui_pixelation.select(settings.ui_pixel_size - 1) _ui_pixelation.select(settings.ui_pixel_size - 1)
_chat_dock.select(1 if settings.chat_dock_right else 0) _chat_dock.select(1 if _chat_dock_right else 0)
_chat_mode.select(1 if settings.chat_mobile_mode else 0) _chat_mode.select(1 if _chat_mobile_mode else 0)
_paint_dock.select(1 if settings.paint_dock_right else 0) _paint_dock.select(1 if _paint_dock_right else 0)
_fullscreen_toggle.set_pressed_no_signal(settings.fullscreen_enabled) _fullscreen_toggle.set_pressed_no_signal(_fullscreen_enabled)
_master_volume_slider.set_value_no_signal(_master_volume * 100.0) _master_volume_slider.set_value_no_signal(_master_volume * 100.0)
_music_volume_slider.set_value_no_signal(_music_volume * 100.0) _music_volume_slider.set_value_no_signal(_music_volume * 100.0)
_effects_volume_slider.set_value_no_signal(_effects_volume * 100.0) _effects_volume_slider.set_value_no_signal(_effects_volume * 100.0)
@ -1084,43 +1066,22 @@ func _set_ui_pixelation(pixel_size: int) -> void:
func _on_chat_dock_selected(index: int) -> void: func _on_chat_dock_selected(index: int) -> void:
_set_presentation_toggle( _chat_dock_right = _chat_dock.get_item_id(index) == 1
&"chat_dock_right",
_chat_dock.get_item_id(index) == 1,
)
func _on_chat_mode_selected(index: int) -> void: func _on_chat_mode_selected(index: int) -> void:
_set_presentation_toggle( _chat_mobile_mode = _chat_mode.get_item_id(index) == 1
&"chat_mobile_mode",
_chat_mode.get_item_id(index) == 1,
)
func _on_paint_dock_selected(index: int) -> void: func _on_paint_dock_selected(index: int) -> void:
_set_presentation_toggle( _paint_dock_right = _paint_dock.get_item_id(index) == 1
&"paint_dock_right",
_paint_dock.get_item_id(index) == 1,
)
func _set_fullscreen(enabled: bool) -> void: func _set_fullscreen(enabled: bool) -> void:
_set_presentation_toggle(&"fullscreen_enabled", enabled) _fullscreen_enabled = enabled
_refresh_value_labels() _refresh_value_labels()
func _set_presentation_toggle(
property_name: StringName,
enabled: bool,
) -> void:
if _settings_manager == null:
return
var edited: PlayerSettings = _settings_manager.current_settings.copy()
edited.set(property_name, enabled)
if not _settings_manager.apply_settings(edited):
_feedback.text = "failed to save display setting."
func _on_mouse_sensitivity_changed(value: float) -> void: func _on_mouse_sensitivity_changed(value: float) -> void:
_mouse_sensitivity = clampf( _mouse_sensitivity = clampf(
value, value,

View file

@ -85,6 +85,7 @@ unique_name_in_owner = true
custom_minimum_size = Vector2(115, 52) custom_minimum_size = Vector2(115, 52)
layout_mode = 2 layout_mode = 2
focus_mode = 2 focus_mode = 2
focus_neighbor_bottom = NodePath("../../ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/FolderRow/OpenDataFolder")
text = "data" text = "data"
script = ExtResource("3_tab") script = ExtResource("3_tab")
palette_index = 1 palette_index = 1
@ -417,6 +418,7 @@ unique_name_in_owner = true
custom_minimum_size = Vector2(330, 48) custom_minimum_size = Vector2(330, 48)
layout_mode = 2 layout_mode = 2
focus_mode = 2 focus_mode = 2
focus_neighbor_bottom = NodePath("../../BindingButtons/ControllerMapping")
toggle_mode = true toggle_mode = true
text = "off" text = "off"
@ -433,6 +435,8 @@ unique_name_in_owner = true
custom_minimum_size = Vector2(300, 52) custom_minimum_size = Vector2(300, 52)
layout_mode = 2 layout_mode = 2
focus_mode = 2 focus_mode = 2
focus_neighbor_right = NodePath("../KeyboardMapping")
focus_neighbor_top = NodePath("../../Grid/OnScreenKeyboardToggle")
text = "controller binds" text = "controller binds"
[node name="KeyboardMapping" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/ControlsPage/BindingButtons"] [node name="KeyboardMapping" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/ControlsPage/BindingButtons"]
@ -440,6 +444,8 @@ unique_name_in_owner = true
custom_minimum_size = Vector2(300, 52) custom_minimum_size = Vector2(300, 52)
layout_mode = 2 layout_mode = 2
focus_mode = 2 focus_mode = 2
focus_neighbor_left = NodePath("../ControllerMapping")
focus_neighbor_top = NodePath("../../Grid/OnScreenKeyboardToggle")
text = "keyboard binds" text = "keyboard binds"
[node name="AccessibilityPage" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack"] [node name="AccessibilityPage" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack"]
@ -554,6 +560,9 @@ custom_minimum_size = Vector2(0, 44)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
focus_mode = 2 focus_mode = 2
focus_neighbor_right = NodePath("../ChangeDataFolder")
focus_neighbor_top = NodePath("../../../../../../../../TabBar/DataTab")
focus_neighbor_bottom = NodePath("../../CopyPlayerFingerprint")
text = "open data folder" text = "open data folder"
[node name="ChangeDataFolder" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/FolderRow"] [node name="ChangeDataFolder" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/FolderRow"]
@ -562,6 +571,9 @@ custom_minimum_size = Vector2(0, 44)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
focus_mode = 2 focus_mode = 2
focus_neighbor_left = NodePath("../OpenDataFolder")
focus_neighbor_top = NodePath("../../../../../../../../TabBar/DataTab")
focus_neighbor_bottom = NodePath("../../CopyPlayerFingerprint")
text = "change data folder" text = "change data folder"
[node name="CopyPlayerFingerprint" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons"] [node name="CopyPlayerFingerprint" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons"]
@ -569,6 +581,8 @@ unique_name_in_owner = true
custom_minimum_size = Vector2(0, 44) custom_minimum_size = Vector2(0, 44)
layout_mode = 2 layout_mode = 2
focus_mode = 2 focus_mode = 2
focus_neighbor_top = NodePath("../FolderRow/OpenDataFolder")
focus_neighbor_bottom = NodePath("../PlayerIdentityRow/ExportPlayerIdentity")
text = "copy player fingerprint" text = "copy player fingerprint"
[node name="PlayerIdentityRow" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons"] [node name="PlayerIdentityRow" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons"]
@ -581,6 +595,9 @@ custom_minimum_size = Vector2(0, 44)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
focus_mode = 2 focus_mode = 2
focus_neighbor_right = NodePath("../ImportPlayerIdentity")
focus_neighbor_top = NodePath("../../CopyPlayerFingerprint")
focus_neighbor_bottom = NodePath("../../HostIdentityRow/ExportHostIdentity")
text = "export player identity" text = "export player identity"
[node name="ImportPlayerIdentity" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/PlayerIdentityRow"] [node name="ImportPlayerIdentity" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/PlayerIdentityRow"]
@ -589,6 +606,9 @@ custom_minimum_size = Vector2(0, 44)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
focus_mode = 2 focus_mode = 2
focus_neighbor_left = NodePath("../ExportPlayerIdentity")
focus_neighbor_top = NodePath("../../CopyPlayerFingerprint")
focus_neighbor_bottom = NodePath("../../HostIdentityRow/ImportHostIdentity")
text = "import player identity" text = "import player identity"
[node name="HostIdentityRow" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons"] [node name="HostIdentityRow" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons"]
@ -601,6 +621,8 @@ custom_minimum_size = Vector2(0, 44)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
focus_mode = 2 focus_mode = 2
focus_neighbor_right = NodePath("../ImportHostIdentity")
focus_neighbor_top = NodePath("../../PlayerIdentityRow/ExportPlayerIdentity")
text = "export host identity" text = "export host identity"
[node name="ImportHostIdentity" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/HostIdentityRow"] [node name="ImportHostIdentity" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/HostIdentityRow"]
@ -609,6 +631,8 @@ custom_minimum_size = Vector2(0, 44)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
focus_mode = 2 focus_mode = 2
focus_neighbor_left = NodePath("../ExportHostIdentity")
focus_neighbor_top = NodePath("../../PlayerIdentityRow/ImportPlayerIdentity")
text = "import host identity" text = "import host identity"
[node name="FooterGroup" type="MarginContainer" parent="MainPanel/OuterMargin/Layout"] [node name="FooterGroup" type="MarginContainer" parent="MainPanel/OuterMargin/Layout"]

View file

@ -128,8 +128,6 @@ func reset_controller_zone() -> void:
func handle_controller_input(event: InputEvent) -> bool: func handle_controller_input(event: InputEvent) -> bool:
if not _active or not _interactive: if not _active or not _interactive:
return false return false
if _handle_controller_scroll(event):
return true
if event.is_action_pressed("ui_cancel"): if event.is_action_pressed("ui_cancel"):
if _controller_zone == ControllerZone.CLAIMS: if _controller_zone == ControllerZone.CLAIMS:
_controller_zone = ControllerZone.TABS _controller_zone = ControllerZone.TABS
@ -160,37 +158,10 @@ func handle_controller_input(event: InputEvent) -> bool:
return true return true
return false return false
func _process(_delta: float) -> void:
func _handle_controller_scroll(event: InputEvent) -> bool:
var motion := event as InputEventJoypadMotion
if motion == null or _jobs_scroll == null:
return false
var uses_right_y: bool = (
_controller_mapping_manager.event_uses_role(
event,
ControllerMappingManager.ROLE_RIGHT_STICK_Y,
)
if _controller_mapping_manager != null
else motion.axis == JOY_AXIS_RIGHT_Y
)
if not uses_right_y:
return false
return true
func _process(delta: float) -> void:
if _active and _jobs != null: if _active and _jobs != null:
_refresh_label.text = _daily_refresh_text() _refresh_label.text = _daily_refresh_text()
_refresh_forecast(false) _refresh_forecast(false)
var right_y: float = (
_controller_mapping_manager.get_role_axis(
ControllerMappingManager.ROLE_RIGHT_STICK_Y
)
if _controller_mapping_manager != null
else Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y)
)
if _jobs_scroll != null and absf(right_y) >= 0.18:
_jobs_scroll.scroll_vertical += roundi(right_y * 320.0 * delta)
func _build_laptop() -> void: func _build_laptop() -> void:

View file

@ -8,9 +8,6 @@ const UIReferencePresentationType = preload(
const ControllerFocusPresentationType = preload( const ControllerFocusPresentationType = preload(
"res://ui/controller_focus_presentation.gd" "res://ui/controller_focus_presentation.gd"
) )
const ControllerFocusRecoveryType = preload(
"res://ui/controller_focus_recovery.gd"
)
const ControllerMappingManagerType = preload( const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd" "res://settings/controller_mapping_manager.gd"
) )
@ -50,8 +47,6 @@ func _ready() -> void:
Callable(_on_screen_keyboard, "request_for_control"), Callable(_on_screen_keyboard, "request_for_control"),
Callable(_on_screen_keyboard, "is_open"), Callable(_on_screen_keyboard, "is_open"),
) )
var controller_focus_recovery := ControllerFocusRecoveryType.new()
_ui_root.add_child(controller_focus_recovery)
var controller_focus_presentation := ControllerFocusPresentationType.new() var controller_focus_presentation := ControllerFocusPresentationType.new()
_ui_root.add_child(controller_focus_presentation) _ui_root.add_child(controller_focus_presentation)
var root_viewport: Viewport = get_viewport() var root_viewport: Viewport = get_viewport()