From 359a96f6e28935ed6e70da67dda105d9ee066cb3 Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 27 Jul 2026 17:33:53 -0400 Subject: [PATCH] Migrate settings to bubble menus --- ui/pause_menu.gd | 7 +- ui/settings/settings_bubble_page.gd | 237 ++++++++ ui/settings/settings_bubble_page.gd.uid | 1 + ui/settings_panel.gd | 499 ++++++++++++++--- ui/settings_panel.tscn | 712 ++++++++++++++++++------ ui/title_screen.gd | 507 ++++++++++++++++- ui/title_screen.tscn | 67 ++- 7 files changed, 1741 insertions(+), 289 deletions(-) create mode 100644 ui/settings/settings_bubble_page.gd create mode 100644 ui/settings/settings_bubble_page.gd.uid diff --git a/ui/pause_menu.gd b/ui/pause_menu.gd index eb1a30a..57e8d8b 100644 --- a/ui/pause_menu.gd +++ b/ui/pause_menu.gd @@ -149,7 +149,7 @@ func handle_escape() -> bool: if _confirmation_panel.visible: _close_confirmation() elif _settings_panel.visible: - _settings_panel.close_panel() + _settings_panel.handle_back() else: resume() return true @@ -176,7 +176,10 @@ func _open_settings() -> void: if _action_in_progress or _confirmation_panel.visible: return _root_panel.hide() - _settings_panel.open_panel(_settings_manager) + _settings_panel.open_panel( + _settings_manager, + SettingsPanelType.PresentationMode.GAMEPLAY_MODAL + ) func _on_settings_applied() -> void: diff --git a/ui/settings/settings_bubble_page.gd b/ui/settings/settings_bubble_page.gd new file mode 100644 index 0000000..57cea81 --- /dev/null +++ b/ui/settings/settings_bubble_page.gd @@ -0,0 +1,237 @@ +class_name SettingsBubblePage +extends Control + +@export var page_id: StringName +@export var cluster_path: NodePath = ^"BubbleCluster" +@export var bubble_paths: Array[NodePath] = [] +@export var focus_paths: Array[NodePath] = [] +@export var initial_focus_path: NodePath +@export var back_focus_path: NodePath +@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.1, 2.0, 0.01) var outgoing_rise_duration: float = 0.95 +@export_range(0.1, 2.0, 0.01) var incoming_rise_duration: float = 1.05 +@export_range(0.0, 128.0, 1.0) var transition_safe_margin: float = 24.0 + +var _cluster: BubbleCluster +var _bubbles: Array[BubbleButton] = [] +var _focus_bubbles: Array[BubbleButton] = [] +var _transition: Tween +var _transition_generation: int = 0 +var _resting_cluster_position: Vector2 = Vector2.ZERO +var _is_transitioning: bool = false + + +func _ready() -> void: + _cluster = get_node(cluster_path) as BubbleCluster + for path: NodePath in bubble_paths: + var bubble := get_node(path) as BubbleButton + if bubble != null: + _bubbles.append(bubble) + for path: NodePath in focus_paths: + var bubble := get_node(path) as BubbleButton + if bubble != null: + _focus_bubbles.append(bubble) + _cluster.configure(_bubbles) + _configure_focus_order() + resized.connect(_update_layout) + call_deferred("_update_layout") + set_process(false) + + +func show_page(should_focus: bool = true) -> void: + _transition_generation += 1 + _cancel_transition() + show() + _is_transitioning = false + _cluster.position = _resting_cluster_position + set_process(true) + _set_interactive(true) + if should_focus: + focus_initial() + + +func transition_out( + completed: Callable, + duration_override: float = -1.0, +) -> void: + _transition_generation += 1 + _cancel_transition() + _is_transitioning = true + _set_interactive(false) + set_process(true) + var duration: float = ( + duration_override + if duration_override > 0.0 + else outgoing_rise_duration + ) + var generation: int = _transition_generation + _transition = create_tween() + _transition.tween_property( + _cluster, + "position:y", + -_cluster.size.y - transition_safe_margin, + duration + ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + _transition.finished.connect( + _finish_transition_out.bind(generation, completed), + CONNECT_ONE_SHOT + ) + + +func transition_in( + should_focus: bool, + completed: Callable, + duration_override: float = -1.0, +) -> void: + _transition_generation += 1 + _cancel_transition() + show() + _is_transitioning = true + _set_interactive(false) + set_process(true) + var duration: float = ( + duration_override + if duration_override > 0.0 + else incoming_rise_duration + ) + _cluster.position = Vector2( + _resting_cluster_position.x, + size.y + transition_safe_margin + ) + var generation: int = _transition_generation + _transition = create_tween() + _transition.tween_property( + _cluster, + "position", + _resting_cluster_position, + duration + ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + _transition.finished.connect( + _finish_transition_in.bind(generation, should_focus, completed), + CONNECT_ONE_SHOT + ) + + +func hide_page() -> void: + _transition_generation += 1 + _cancel_transition() + _is_transitioning = false + _set_interactive(false) + set_process(false) + hide() + if _cluster != null: + _cluster.position = _resting_cluster_position + + +func focus_initial() -> void: + var initial := get_node_or_null(initial_focus_path) as Control + if initial != null and initial.visible and initial.focus_mode != Control.FOCUS_NONE: + initial.grab_focus() + + +func focus_back() -> void: + var back := get_node_or_null(back_focus_path) as Control + if back != null and back.visible and back.focus_mode != Control.FOCUS_NONE: + back.grab_focus() + + +func get_page_id() -> StringName: + return page_id + + +func _process(delta: float) -> void: + if visible: + _cluster.advance_motion(delta) + + +func _update_layout() -> void: + if not is_node_ready() or _cluster == null: + return + var compact: bool = ( + size.x < compact_width_threshold + or size.y < compact_height_threshold + ) + 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( + minf(size.x, layout_maximum.x), + minf(size.y, layout_maximum.y) + ) + field_size.x = maxf(1.0, field_size.x) + field_size.y = maxf(1.0, field_size.y) + _resting_cluster_position = (size - field_size) * 0.5 + if not _is_transitioning: + _cluster.position = _resting_cluster_position + _cluster.size = field_size + _cluster.apply_layout(field_size, compact) + + +func _configure_focus_order() -> void: + for bubble: BubbleButton in _bubbles: + bubble.focus_mode = Control.FOCUS_NONE + for index: int in _focus_bubbles.size(): + var bubble: BubbleButton = _focus_bubbles[index] + if index > 0: + bubble.focus_neighbor_top = bubble.get_path_to( + _focus_bubbles[index - 1] + ) + bubble.focus_neighbor_left = bubble.focus_neighbor_top + if index + 1 < _focus_bubbles.size(): + bubble.focus_neighbor_bottom = bubble.get_path_to( + _focus_bubbles[index + 1] + ) + bubble.focus_neighbor_right = bubble.focus_neighbor_bottom + + +func _set_interactive(interactive: bool) -> void: + mouse_filter = ( + Control.MOUSE_FILTER_PASS + if interactive + else Control.MOUSE_FILTER_IGNORE + ) + for bubble: BubbleButton in _focus_bubbles: + bubble.focus_mode = ( + Control.FOCUS_ALL if interactive else Control.FOCUS_NONE + ) + bubble.mouse_filter = ( + Control.MOUSE_FILTER_STOP + if interactive + else Control.MOUSE_FILTER_IGNORE + ) + + +func _finish_transition_out(generation: int, completed: Callable) -> void: + if generation != _transition_generation or not visible: + return + _transition = null + _is_transitioning = false + hide() + _cluster.position = _resting_cluster_position + completed.call() + + +func _finish_transition_in( + generation: int, + should_focus: bool, + completed: Callable, +) -> void: + if generation != _transition_generation or not visible: + return + _transition = null + _is_transitioning = false + _cluster.position = _resting_cluster_position + set_process(true) + _set_interactive(true) + if should_focus: + focus_initial() + completed.call() + + +func _cancel_transition() -> void: + if _transition != null: + _transition.kill() + _transition = null diff --git a/ui/settings/settings_bubble_page.gd.uid b/ui/settings/settings_bubble_page.gd.uid new file mode 100644 index 0000000..77b26e5 --- /dev/null +++ b/ui/settings/settings_bubble_page.gd.uid @@ -0,0 +1 @@ +uid://cwgitrlhjkepy diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index 1ec189d..8c62588 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -1,86 +1,335 @@ class_name SettingsPanel -extends PanelContainer +extends Control -const MAX_PANEL_SIZE: Vector2 = Vector2(720.0, 600.0) +const MAX_PANEL_SIZE: Vector2 = Vector2(960.0, 700.0) const PANEL_EDGE_MARGIN: float = 16.0 +const PAGE_ROOT: StringName = &"root" +const PAGE_DISPLAY: StringName = &"display" +const PAGE_CONTROLS: StringName = &"controls" +const PAGE_ACCESSIBILITY: StringName = &"accessibility" +const TITLE_HOST_OUTGOING_DURATION: float = 1.70 +const TITLE_HOST_INCOMING_DURATION: float = 1.80 +const PIXELATION_NAMES: PackedStringArray = [ + "legible", + "cute", + "retro", + "hardcore", + "wtf", +] const SettingsManagerType = preload( "res://settings/player_settings_manager.gd" ) signal applied signal closed +signal opened signal crisp_reset_focus_requested signal panel_visibility_changed(is_visible: bool) -@onready var _auto_click_toggle: CheckButton = %AutoClickToggle -@onready var _auto_click_interval: HSlider = %AutoClickInterval -@onready var _auto_click_interval_value: Label = %AutoClickIntervalValue -@onready var _mouse_sensitivity: HSlider = %MouseSensitivity -@onready var _mouse_sensitivity_value: Label = %MouseSensitivityValue -@onready var _controller_sensitivity: HSlider = %ControllerSensitivity -@onready var _controller_sensitivity_value: Label = %ControllerSensitivityValue -@onready var _invert_y_toggle: CheckButton = %InvertYToggle -@onready var _world_pixel_size: HSlider = %WorldPixelSize -@onready var _world_pixel_size_value: Label = %WorldPixelSizeValue -@onready var _ui_pixel_size: HSlider = %UIPixelSize -@onready var _ui_pixel_size_value: Label = %UIPixelSizeValue +enum PresentationMode { + TITLE_EMBEDDED, + GAMEPLAY_MODAL, +} + +@onready var _backdrop: ColorRect = $Backdrop +@onready var _root_page: SettingsBubblePage = %RootPage +@onready var _display_page: SettingsBubblePage = %DisplayPage +@onready var _controls_page: SettingsBubblePage = %ControlsPage +@onready var _accessibility_page: SettingsBubblePage = %AccessibilityPage @onready var _feedback: Label = %SettingsFeedback +@onready var _world_value: BubbleButton = %WorldValue +@onready var _ui_value: BubbleButton = %UIValue +@onready var _mouse_value: BubbleButton = %MouseValue +@onready var _controller_value: BubbleButton = %ControllerValue +@onready var _invert_y_toggle: BubbleButton = %InvertYToggle +@onready var _auto_click_toggle: BubbleButton = %AutoClickToggle +@onready var _auto_click_interval: BubbleButton = %AutoClickIntervalValue + +@onready var _world_options: Array[BubbleButton] = [ + %WorldLegible, + %WorldCute, + %WorldRetro, + %WorldHardcore, + %WorldWtf, +] +@onready var _ui_options: Array[BubbleButton] = [ + %UILegible, + %UICute, + %UIRetro, + %UIHardcore, + %UIWtf, +] + var _settings_manager: SettingsManagerType -var _loading_controls: bool = false var _effective_ui_pixel_size: int = PlayerSettings.DEFAULT_UI_PIXEL_SIZE +var _page_stack: Array[StringName] = [] +var _pages: Dictionary[StringName, SettingsBubblePage] = {} +var _page_transition_generation: int = 0 +var _page_transition_active: bool = false +var _presentation_mode: PresentationMode = PresentationMode.GAMEPLAY_MODAL +var _auto_click_enabled: bool = false +var _auto_click_interval_value: float = 0.20 +var _mouse_sensitivity: float = 0.005 +var _controller_sensitivity: float = 2.5 +var _invert_camera_y: bool = false func _ready() -> void: + _pages = { + PAGE_ROOT: _root_page, + PAGE_DISPLAY: _display_page, + PAGE_CONTROLS: _controls_page, + PAGE_ACCESSIBILITY: _accessibility_page, + } + %DisplayCategory.pressed.connect(_push_page.bind(PAGE_DISPLAY)) + %ControlsCategory.pressed.connect(_push_page.bind(PAGE_CONTROLS)) + %AccessibilityCategory.pressed.connect( + _push_page.bind(PAGE_ACCESSIBILITY) + ) %ApplySettingsButton.pressed.connect(_apply_settings) - %CancelSettingsButton.pressed.connect(close_panel) - _auto_click_interval.value_changed.connect(_refresh_value_labels) - _mouse_sensitivity.value_changed.connect(_refresh_value_labels) - _controller_sensitivity.value_changed.connect(_refresh_value_labels) - _world_pixel_size.value_changed.connect(_on_pixel_size_changed) - _ui_pixel_size.value_changed.connect(_on_pixel_size_changed) - %CancelSettingsButton.gui_input.connect(_on_back_button_gui_input) - get_viewport().size_changed.connect(_refresh_panel_size) + %RootBackButton.pressed.connect(close_panel) + %DisplayBackButton.pressed.connect(handle_back) + %ControlsBackButton.pressed.connect(handle_back) + %AccessibilityBackButton.pressed.connect(handle_back) + %RootBackButton.gui_input.connect(_on_back_bubble_gui_input) + %DisplayBackButton.gui_input.connect(_on_back_bubble_gui_input) + %ControlsBackButton.gui_input.connect(_on_back_bubble_gui_input) + %AccessibilityBackButton.gui_input.connect(_on_back_bubble_gui_input) + for index: int in _world_options.size(): + _world_options[index].pressed.connect( + _set_world_pixelation.bind(index + 1) + ) + for index: int in _ui_options.size(): + _ui_options[index].pressed.connect( + _set_ui_pixelation.bind(index + 1) + ) + %MouseDecrease.pressed.connect(_adjust_mouse_sensitivity.bind(-1)) + %MouseIncrease.pressed.connect(_adjust_mouse_sensitivity.bind(1)) + _mouse_value.pressed.connect(_adjust_mouse_sensitivity.bind(1)) + %ControllerDecrease.pressed.connect( + _adjust_controller_sensitivity.bind(-1) + ) + %ControllerIncrease.pressed.connect( + _adjust_controller_sensitivity.bind(1) + ) + _controller_value.pressed.connect( + _adjust_controller_sensitivity.bind(1) + ) + _invert_y_toggle.pressed.connect(_toggle_invert_y) + _auto_click_toggle.pressed.connect(_toggle_auto_click) + %IntervalDecrease.pressed.connect(_adjust_auto_click_interval.bind(-1)) + %IntervalIncrease.pressed.connect(_adjust_auto_click_interval.bind(1)) + _auto_click_interval.pressed.connect( + _adjust_auto_click_interval.bind(1) + ) + _mouse_value.gui_input.connect( + _on_continuous_value_input.bind(&"mouse") + ) + _controller_value.gui_input.connect( + _on_continuous_value_input.bind(&"controller") + ) + _auto_click_interval.gui_input.connect( + _on_continuous_value_input.bind(&"interval") + ) + resized.connect(_refresh_panel_size) var parent_control := get_parent() as Control if parent_control != null: parent_control.resized.connect(_refresh_panel_size) + for page: SettingsBubblePage in _pages.values(): + page.hide_page() call_deferred("_refresh_panel_size") -func open_panel(settings_manager: SettingsManagerType) -> void: +func open_panel( + settings_manager: SettingsManagerType, + presentation_mode: PresentationMode = PresentationMode.GAMEPLAY_MODAL, +) -> void: + _cancel_page_transition() + _presentation_mode = presentation_mode + _backdrop.visible = presentation_mode == PresentationMode.GAMEPLAY_MODAL _settings_manager = settings_manager _load_controls() _feedback.text = "" show() + _page_stack.clear() + _page_stack.append(PAGE_ROOT) panel_visibility_changed.emit(true) - _auto_click_toggle.grab_focus() + if presentation_mode == PresentationMode.TITLE_EMBEDDED: + for page: SettingsBubblePage in _pages.values(): + page.hide_page() + _page_transition_generation += 1 + _page_transition_active = true + var generation: int = _page_transition_generation + _root_page.transition_in( + true, + _finish_embedded_open.bind(generation), + TITLE_HOST_INCOMING_DURATION + ) + else: + _show_active_page(true) + opened.emit() func close_panel() -> void: if not visible: return + if _presentation_mode == PresentationMode.TITLE_EMBEDDED: + if _page_transition_active: + return + _begin_embedded_close(false) + return + _finish_panel_close(false) + + +func _finish_panel_close(applied_result: bool) -> void: + _cancel_page_transition() + for page: SettingsBubblePage in _pages.values(): + page.hide_page() + _page_stack.clear() hide() panel_visibility_changed.emit(false) - _load_controls() - closed.emit() + if applied_result: + applied.emit() + else: + _load_controls() + closed.emit() + + +func handle_back() -> void: + if _page_transition_active: + return + if _page_stack.size() > 1: + _start_page_transition(_page_stack[-2], false) + return + close_panel() + + +func get_active_page_id() -> StringName: + return _page_stack.back() if not _page_stack.is_empty() else StringName() + + +func _push_page(page_id: StringName) -> void: + if ( + _page_transition_active + or not _pages.has(page_id) + or get_active_page_id() == page_id + ): + return + _start_page_transition(page_id, true) + + +func _start_page_transition(page_id: StringName, push_page: bool) -> void: + var outgoing_page: SettingsBubblePage = _get_active_page() + var incoming_page: SettingsBubblePage = _pages.get(page_id) + if outgoing_page == null or incoming_page == null: + return + _page_transition_generation += 1 + _page_transition_active = true + var generation: int = _page_transition_generation + outgoing_page.transition_out( + _finish_outgoing_page.bind( + generation, + page_id, + push_page, + incoming_page + ) + ) + + +func _finish_outgoing_page( + generation: int, + page_id: StringName, + push_page: bool, + incoming_page: SettingsBubblePage, +) -> void: + if generation != _page_transition_generation or not _page_transition_active: + return + if push_page: + _page_stack.append(page_id) + else: + _page_stack.pop_back() + for page: SettingsBubblePage in _pages.values(): + if page != incoming_page: + page.hide_page() + incoming_page.transition_in( + true, + _finish_incoming_page.bind(generation) + ) + + +func _finish_incoming_page(generation: int) -> void: + if generation != _page_transition_generation or not _page_transition_active: + return + _page_transition_active = false + + +func _finish_embedded_open(generation: int) -> void: + if generation != _page_transition_generation or not _page_transition_active: + return + _page_transition_active = false + opened.emit() + + +func _begin_embedded_close(applied_result: bool) -> void: + var active_page: SettingsBubblePage = _get_active_page() + if active_page == null: + return + _page_transition_generation += 1 + _page_transition_active = true + var generation: int = _page_transition_generation + active_page.transition_out( + _finish_embedded_close.bind(generation, applied_result), + TITLE_HOST_OUTGOING_DURATION + ) + + +func _finish_embedded_close( + generation: int, + applied_result: bool, +) -> void: + if generation != _page_transition_generation or not _page_transition_active: + return + _finish_panel_close(applied_result) + + +func _cancel_page_transition() -> void: + _page_transition_generation += 1 + _page_transition_active = false + + +func _show_active_page(should_focus: bool) -> void: + for page_id: StringName in _pages: + var page: SettingsBubblePage = _pages[page_id] + if page_id == get_active_page_id(): + page.show_page(should_focus) + else: + page.hide_page() + + +func _get_active_page() -> SettingsBubblePage: + return _pages.get(get_active_page_id()) as SettingsBubblePage func _apply_settings() -> void: + if _page_transition_active: + return if _settings_manager == null: _feedback.text = "settings are unavailable." return var edited := PlayerSettings.new() - edited.auto_click_enabled = _auto_click_toggle.button_pressed - edited.auto_click_interval = float(_auto_click_interval.value) - edited.mouse_camera_sensitivity = float(_mouse_sensitivity.value) - edited.controller_camera_sensitivity = float(_controller_sensitivity.value) - edited.invert_camera_y = _invert_y_toggle.button_pressed - edited.world_pixel_size = roundi(_world_pixel_size.value) - edited.ui_pixel_size = roundi(_ui_pixel_size.value) + edited.auto_click_enabled = _auto_click_enabled + edited.auto_click_interval = _auto_click_interval_value + edited.mouse_camera_sensitivity = _mouse_sensitivity + edited.controller_camera_sensitivity = _controller_sensitivity + edited.invert_camera_y = _invert_camera_y + edited.world_pixel_size = _settings_manager.current_settings.world_pixel_size + edited.ui_pixel_size = _settings_manager.current_settings.ui_pixel_size if _settings_manager.apply_settings(edited): - hide() - panel_visibility_changed.emit(false) - applied.emit() + if _presentation_mode == PresentationMode.TITLE_EMBEDDED: + _begin_embedded_close(true) + else: + _finish_panel_close(true) else: _feedback.text = "failed to save settings." @@ -88,54 +337,139 @@ func _apply_settings() -> void: func _load_controls() -> void: if _settings_manager == null or not is_node_ready(): return - _loading_controls = true var settings: PlayerSettings = _settings_manager.current_settings - _auto_click_toggle.button_pressed = settings.auto_click_enabled - _auto_click_interval.value = settings.auto_click_interval - _mouse_sensitivity.value = settings.mouse_camera_sensitivity - _controller_sensitivity.value = settings.controller_camera_sensitivity - _invert_y_toggle.button_pressed = settings.invert_camera_y - _world_pixel_size.value = settings.world_pixel_size - _ui_pixel_size.value = settings.ui_pixel_size - _loading_controls = false - _refresh_value_labels(0.0) + _auto_click_enabled = settings.auto_click_enabled + _auto_click_interval_value = settings.auto_click_interval + _mouse_sensitivity = settings.mouse_camera_sensitivity + _controller_sensitivity = settings.controller_camera_sensitivity + _invert_camera_y = settings.invert_camera_y + _refresh_value_labels() -func _refresh_value_labels(_unused: float) -> void: - _auto_click_interval_value.text = "%.2f s" % _auto_click_interval.value - _mouse_sensitivity_value.text = "%.4f" % _mouse_sensitivity.value - _controller_sensitivity_value.text = "%.1f" % _controller_sensitivity.value - _world_pixel_size_value.text = _pixel_size_label( - roundi(_world_pixel_size.value), - true +func _refresh_value_labels() -> void: + if _settings_manager == null: + return + var settings: PlayerSettings = _settings_manager.current_settings + _world_value.text = ( + "world\npixelation\n" + + _pixel_size_label(settings.world_pixel_size) ) - _ui_pixel_size_value.text = _pixel_size_label( - roundi(_ui_pixel_size.value), - false + _ui_value.text = ( + "ui\npixelation\n" + + _pixel_size_label(settings.ui_pixel_size) ) + _mouse_value.text = "mouse\nsensitivity\n%.4f" % _mouse_sensitivity + _controller_value.text = ( + "controller\nsensitivity\n%.1f" % _controller_sensitivity + ) + _invert_y_toggle.text = ( + "invert\nvertical\ncamera\n" + + ("on" if _invert_camera_y else "off") + ) + _auto_click_toggle.text = ( + "accessibility\nauto-click\n" + + ("on" if _auto_click_enabled else "off") + ) + _auto_click_interval.text = ( + "auto-click\ninterval\n%.2f s" % _auto_click_interval_value + ) + for index: int in _world_options.size(): + _world_options[index].button_pressed = ( + index + 1 == settings.world_pixel_size + ) + for index: int in _ui_options.size(): + _ui_options[index].button_pressed = index + 1 == settings.ui_pixel_size -func _on_pixel_size_changed(_unused: float) -> void: - _refresh_value_labels(0.0) - if _loading_controls or _settings_manager == null: +func _set_world_pixelation(pixel_size: int) -> void: + if _settings_manager == null: return if not _settings_manager.apply_pixelation( - roundi(_world_pixel_size.value), - roundi(_ui_pixel_size.value) + pixel_size, + _settings_manager.current_settings.ui_pixel_size ): _feedback.text = "failed to save pixelation settings." + return + _refresh_value_labels() -func _pixel_size_label(pixel_size: int, _is_world: bool) -> String: - var names: PackedStringArray = [ - "legible", - "cute", - "retro", - "hardcore", - "wtf", - ] - var index: int = clampi(pixel_size - 1, 0, names.size() - 1) - return names[index] +func _set_ui_pixelation(pixel_size: int) -> void: + if _settings_manager == null: + return + if not _settings_manager.apply_pixelation( + _settings_manager.current_settings.world_pixel_size, + pixel_size + ): + _feedback.text = "failed to save pixelation settings." + return + _refresh_value_labels() + + +func _adjust_mouse_sensitivity(direction: int) -> void: + _mouse_sensitivity = clampf( + _mouse_sensitivity + float(direction) * 0.0005, + PlayerSettings.MIN_MOUSE_SENSITIVITY, + PlayerSettings.MAX_MOUSE_SENSITIVITY + ) + _refresh_value_labels() + + +func _adjust_controller_sensitivity(direction: int) -> void: + _controller_sensitivity = clampf( + _controller_sensitivity + float(direction) * 0.1, + PlayerSettings.MIN_CONTROLLER_SENSITIVITY, + PlayerSettings.MAX_CONTROLLER_SENSITIVITY + ) + _refresh_value_labels() + + +func _adjust_auto_click_interval(direction: int) -> void: + _auto_click_interval_value = clampf( + _auto_click_interval_value + float(direction) * 0.01, + PlayerSettings.MIN_AUTO_CLICK_INTERVAL, + PlayerSettings.MAX_AUTO_CLICK_INTERVAL + ) + _refresh_value_labels() + + +func _toggle_invert_y() -> void: + _invert_camera_y = not _invert_camera_y + _refresh_value_labels() + + +func _toggle_auto_click() -> void: + _auto_click_enabled = not _auto_click_enabled + _refresh_value_labels() + + +func _on_continuous_value_input( + event: InputEvent, + control_id: StringName, +) -> void: + var direction: int = 0 + if event.is_action_pressed("ui_left"): + direction = -1 + elif event.is_action_pressed("ui_right"): + direction = 1 + if direction == 0: + return + match control_id: + &"mouse": + _adjust_mouse_sensitivity(direction) + &"controller": + _adjust_controller_sensitivity(direction) + &"interval": + _adjust_auto_click_interval(direction) + accept_event() + + +func _pixel_size_label(pixel_size: int) -> String: + var index: int = clampi( + pixel_size - 1, + 0, + PIXELATION_NAMES.size() - 1 + ) + return PIXELATION_NAMES[index] func set_effective_ui_pixel_size(pixel_size: int) -> void: @@ -145,11 +479,19 @@ func set_effective_ui_pixel_size(pixel_size: int) -> void: PlayerSettings.MAX_UI_PIXEL_SIZE ) if is_node_ready(): - _refresh_value_labels(0.0) + _refresh_value_labels() func focus_back_button() -> void: - %CancelSettingsButton.grab_focus() + var active_page: SettingsBubblePage = _get_active_page() + if active_page != null: + active_page.focus_back() + + +func _on_back_bubble_gui_input(event: InputEvent) -> void: + if event.is_action_pressed("ui_down") or event.is_action_pressed("ui_focus_next"): + crisp_reset_focus_requested.emit() + accept_event() func refresh_open_panel() -> void: @@ -157,15 +499,6 @@ func refresh_open_panel() -> void: _load_controls() -func _on_back_button_gui_input(event: InputEvent) -> void: - if ( - event.is_action_pressed("ui_down") - or event.is_action_pressed("ui_focus_next") - ): - crisp_reset_focus_requested.emit() - accept_event() - - func _refresh_panel_size() -> void: if not is_node_ready(): return diff --git a/ui/settings_panel.tscn b/ui/settings_panel.tscn index 0d708cb..a6bc71c 100644 --- a/ui/settings_panel.tscn +++ b/ui/settings_panel.tscn @@ -1,195 +1,563 @@ -[gd_scene load_steps=3 format=3] +[gd_scene load_steps=7 format=3] -[ext_resource type="Script" path="res://ui/settings_panel.gd" id="1_script"] +[ext_resource type="Script" path="res://ui/settings_panel.gd" id="1_panel"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] +[ext_resource type="Script" path="res://ui/settings/settings_bubble_page.gd" id="3_page"] +[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_button.tscn" id="4_bubble"] +[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_cluster.gd" id="5_cluster"] +[ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="6_profile"] -[node name="SettingsPanel" type="PanelContainer"] +[node name="SettingsPanel" type="Control"] visible = false -custom_minimum_size = Vector2(720, 600) +custom_minimum_size = Vector2(960, 700) +layout_mode = 3 +anchors_preset = 0 theme = ExtResource("2_theme") -script = ExtResource("1_script") +script = ExtResource("1_panel") -[node name="Margin" type="MarginContainer" parent="."] -layout_mode = 2 -theme_override_constants/margin_left = 28 -theme_override_constants/margin_top = 24 -theme_override_constants/margin_right = 28 -theme_override_constants/margin_bottom = 24 +[node name="Backdrop" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +color = Color(0.025, 0.085, 0.115, 0.72) -[node name="Scroll" type="ScrollContainer" parent="Margin"] -layout_mode = 2 -horizontal_scroll_mode = 0 - -[node name="Content" type="VBoxContainer" parent="Margin/Scroll"] -layout_mode = 2 -size_flags_horizontal = 3 -theme_override_constants/separation = 12 - -[node name="Heading" type="Label" parent="Margin/Scroll/Content"] -layout_mode = 2 -theme_override_font_sizes/font_size = 39 -text = "settings" +[node name="SettingsFeedback" type="Label" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 12 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -42.0 +offset_bottom = -8.0 +grow_horizontal = 2 +grow_vertical = 0 +mouse_filter = 2 horizontal_alignment = 1 +vertical_alignment = 1 -[node name="AutoClickToggle" type="CheckButton" parent="Margin/Scroll/Content"] +[node name="RootPage" type="Control" parent="."] unique_name_in_owner = true -layout_mode = 2 -text = "accessibility auto-click" +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("3_page") +page_id = &"root" +bubble_paths = Array[NodePath]([NodePath("BubbleCluster/DisplayCategory"), NodePath("BubbleCluster/ControlsCategory"), NodePath("BubbleCluster/AccessibilityCategory"), NodePath("BubbleCluster/ApplySettingsButton"), NodePath("BubbleCluster/RootBackButton")]) +focus_paths = Array[NodePath]([NodePath("BubbleCluster/DisplayCategory"), NodePath("BubbleCluster/ControlsCategory"), NodePath("BubbleCluster/AccessibilityCategory"), NodePath("BubbleCluster/ApplySettingsButton"), NodePath("BubbleCluster/RootBackButton")]) +initial_focus_path = NodePath("BubbleCluster/DisplayCategory") +back_focus_path = NodePath("BubbleCluster/RootBackButton") +compact_maximum_layout_size = Vector2(544, 400) -[node name="AutoClickHelp" type="Label" parent="Margin/Scroll/Content"] -layout_mode = 2 -text = "lower intervals click barriers faster while held." -autowrap_mode = 2 +[node name="BubbleCluster" type="Control" parent="RootPage"] +layout_mode = 0 +offset_right = 720.0 +offset_bottom = 520.0 +script = ExtResource("5_cluster") +profile = ExtResource("6_profile") +desktop_reference_size = Vector2(720, 520) +compact_reference_size = Vector2(608, 448) -[node name="AutoClickRow" type="HBoxContainer" parent="Margin/Scroll/Content"] -layout_mode = 2 -theme_override_constants/separation = 12 - -[node name="Label" type="Label" parent="Margin/Scroll/Content/AutoClickRow"] -custom_minimum_size = Vector2(260, 0) -layout_mode = 2 -text = "auto-click interval" -autowrap_mode = 2 - -[node name="AutoClickInterval" type="HSlider" parent="Margin/Scroll/Content/AutoClickRow"] +[node name="DisplayCategory" parent="RootPage/BubbleCluster" instance=ExtResource("4_bubble")] unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -min_value = 0.1 -max_value = 0.5 -step = 0.01 -value = 0.2 +custom_minimum_size = Vector2(0, 0) +offset_left = 135.0 +offset_top = 90.0 +offset_right = 305.0 +offset_bottom = 250.0 +text = "display" +neutral_size = Vector2(170, 160) +desktop_anchor = Vector2(280, 180) +compact_anchor = Vector2(220, 150) +compact_minimum_size = Vector2(130, 124) +minimum_font_size = 18 +maximum_font_size = 27 +motion_phase = 0.35 -[node name="AutoClickIntervalValue" type="Label" parent="Margin/Scroll/Content/AutoClickRow"] +[node name="ControlsCategory" parent="RootPage/BubbleCluster" instance=ExtResource("4_bubble")] unique_name_in_owner = true -custom_minimum_size = Vector2(100, 0) -layout_mode = 2 -text = "0.20 s" +custom_minimum_size = Vector2(0, 0) +offset_left = 377.0 +offset_top = 96.0 +offset_right = 533.0 +offset_bottom = 244.0 +text = "controls" +neutral_size = Vector2(156, 148) +desktop_anchor = Vector2(440, 180) +compact_anchor = Vector2(385, 150) +compact_minimum_size = Vector2(124, 118) +minimum_font_size = 17 +maximum_font_size = 25 +motion_phase = 1.45 -[node name="MouseRow" type="HBoxContainer" parent="Margin/Scroll/Content"] -layout_mode = 2 -theme_override_constants/separation = 12 - -[node name="Label" type="Label" parent="Margin/Scroll/Content/MouseRow"] -custom_minimum_size = Vector2(260, 0) -layout_mode = 2 -text = "mouse camera sensitivity" -autowrap_mode = 2 - -[node name="MouseSensitivity" type="HSlider" parent="Margin/Scroll/Content/MouseRow"] +[node name="AccessibilityCategory" parent="RootPage/BubbleCluster" instance=ExtResource("4_bubble")] unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -min_value = 0.001 -max_value = 0.012 -step = 0.0005 -value = 0.005 +custom_minimum_size = Vector2(0, 0) +offset_left = 240.0 +offset_top = 262.0 +offset_right = 420.0 +offset_bottom = 428.0 +text = "accessibility" +neutral_size = Vector2(180, 168) +desktop_anchor = Vector2(367, 328) +compact_anchor = Vector2(307, 311) +compact_minimum_size = Vector2(150, 142) +minimum_font_size = 17 +maximum_font_size = 27 +motion_phase = 2.65 -[node name="MouseSensitivityValue" type="Label" parent="Margin/Scroll/Content/MouseRow"] +[node name="ApplySettingsButton" parent="RootPage/BubbleCluster" instance=ExtResource("4_bubble")] unique_name_in_owner = true -custom_minimum_size = Vector2(100, 0) -layout_mode = 2 -text = "0.0050" - -[node name="ControllerRow" type="HBoxContainer" parent="Margin/Scroll/Content"] -layout_mode = 2 -theme_override_constants/separation = 12 - -[node name="Label" type="Label" parent="Margin/Scroll/Content/ControllerRow"] -custom_minimum_size = Vector2(260, 0) -layout_mode = 2 -text = "controller camera sensitivity" -autowrap_mode = 2 - -[node name="ControllerSensitivity" type="HSlider" parent="Margin/Scroll/Content/ControllerRow"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -min_value = 0.5 -max_value = 5.0 -step = 0.1 -value = 2.5 - -[node name="ControllerSensitivityValue" type="Label" parent="Margin/Scroll/Content/ControllerRow"] -unique_name_in_owner = true -custom_minimum_size = Vector2(100, 0) -layout_mode = 2 -text = "2.5" - -[node name="InvertYToggle" type="CheckButton" parent="Margin/Scroll/Content"] -unique_name_in_owner = true -layout_mode = 2 -text = "invert vertical camera" - -[node name="WorldPixelRow" type="HBoxContainer" parent="Margin/Scroll/Content"] -layout_mode = 2 -theme_override_constants/separation = 12 - -[node name="Label" type="Label" parent="Margin/Scroll/Content/WorldPixelRow"] -custom_minimum_size = Vector2(260, 0) -layout_mode = 2 -text = "world pixelation" -autowrap_mode = 2 - -[node name="WorldPixelSize" type="HSlider" parent="Margin/Scroll/Content/WorldPixelRow"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -min_value = 1.0 -max_value = 5.0 -step = 1.0 -value = 3.0 - -[node name="WorldPixelSizeValue" type="Label" parent="Margin/Scroll/Content/WorldPixelRow"] -unique_name_in_owner = true -custom_minimum_size = Vector2(170, 0) -layout_mode = 2 -text = "retro" - -[node name="UIPixelRow" type="HBoxContainer" parent="Margin/Scroll/Content"] -layout_mode = 2 -theme_override_constants/separation = 12 - -[node name="Label" type="Label" parent="Margin/Scroll/Content/UIPixelRow"] -custom_minimum_size = Vector2(260, 0) -layout_mode = 2 -text = "ui pixelation" -autowrap_mode = 2 - -[node name="UIPixelSize" type="HSlider" parent="Margin/Scroll/Content/UIPixelRow"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -min_value = 1.0 -max_value = 5.0 -step = 1.0 -value = 3.0 - -[node name="UIPixelSizeValue" type="Label" parent="Margin/Scroll/Content/UIPixelRow"] -unique_name_in_owner = true -custom_minimum_size = Vector2(170, 0) -layout_mode = 2 -text = "retro" - -[node name="SettingsFeedback" type="Label" parent="Margin/Scroll/Content"] -unique_name_in_owner = true -custom_minimum_size = Vector2(0, 44) -layout_mode = 2 -horizontal_alignment = 1 - -[node name="Buttons" type="HBoxContainer" parent="Margin/Scroll/Content"] -layout_mode = 2 -theme_override_constants/separation = 12 -alignment = 2 - -[node name="ApplySettingsButton" type="Button" parent="Margin/Scroll/Content/Buttons"] -unique_name_in_owner = true -custom_minimum_size = Vector2(150, 64) -layout_mode = 2 +custom_minimum_size = Vector2(0, 0) +offset_left = 474.0 +offset_top = 308.0 +offset_right = 586.0 +offset_bottom = 412.0 text = "apply" +neutral_size = Vector2(112, 104) +desktop_anchor = Vector2(511, 332) +compact_anchor = Vector2(470, 330) +compact_minimum_size = Vector2(94, 90) +minimum_font_size = 15 +maximum_font_size = 19 +motion_phase = 3.7 -[node name="CancelSettingsButton" type="Button" parent="Margin/Scroll/Content/Buttons"] +[node name="RootBackButton" parent="RootPage/BubbleCluster" instance=ExtResource("4_bubble")] unique_name_in_owner = true -custom_minimum_size = Vector2(150, 64) -layout_mode = 2 +custom_minimum_size = Vector2(0, 0) +offset_left = 70.0 +offset_top = 343.0 +offset_right = 170.0 +offset_bottom = 437.0 text = "back" +neutral_size = Vector2(100, 94) +desktop_anchor = Vector2(220, 300) +compact_anchor = Vector2(155, 275) +compact_minimum_size = Vector2(84, 80) +minimum_font_size = 14 +maximum_font_size = 17 +motion_phase = 4.8 + +[node name="DisplayPage" type="Control" parent="."] +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 +script = ExtResource("3_page") +page_id = &"display" +bubble_paths = Array[NodePath]([NodePath("BubbleCluster/WorldValue"), NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UIValue"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/DisplayBackButton")]) +focus_paths = Array[NodePath]([NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/DisplayBackButton")]) +initial_focus_path = NodePath("BubbleCluster/WorldRetro") +back_focus_path = NodePath("BubbleCluster/DisplayBackButton") +compact_maximum_layout_size = Vector2(544, 400) + +[node name="BubbleCluster" type="Control" parent="DisplayPage"] +layout_mode = 0 +offset_right = 720.0 +offset_bottom = 520.0 +script = ExtResource("5_cluster") +profile = ExtResource("6_profile") +desktop_reference_size = Vector2(720, 520) +compact_reference_size = Vector2(608, 448) + +[node name="WorldValue" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +mouse_filter = 2 +focus_mode = 0 +text = "world\npixelation\nretro" +neutral_size = Vector2(160, 150) +desktop_anchor = Vector2(160, 240) +compact_anchor = Vector2(175, 210) +compact_minimum_size = Vector2(132, 126) +minimum_font_size = 15 +maximum_font_size = 25 +motion_phase = 0.2 + +[node name="WorldLegible" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +toggle_mode = true +text = "legible" +neutral_size = Vector2(90, 84) +desktop_anchor = Vector2(55, 175) +compact_anchor = Vector2(57, 135) +compact_minimum_size = Vector2(76, 72) +minimum_font_size = 12 +maximum_font_size = 15 +motion_phase = 0.8 + +[node name="WorldCute" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +toggle_mode = true +text = "cute" +neutral_size = Vector2(82, 78) +desktop_anchor = Vector2(130, 124) +compact_anchor = Vector2(145, 78) +compact_minimum_size = Vector2(70, 68) +minimum_font_size = 12 +maximum_font_size = 14 +motion_phase = 1.35 + +[node name="WorldRetro" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +toggle_mode = true +button_pressed = true +text = "retro" +neutral_size = Vector2(84, 80) +desktop_anchor = Vector2(237, 149) +compact_anchor = Vector2(272, 115) +compact_minimum_size = Vector2(72, 68) +minimum_font_size = 12 +maximum_font_size = 14 +motion_phase = 1.9 + +[node name="WorldHardcore" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +toggle_mode = true +text = "hardcore" +neutral_size = Vector2(100, 94) +desktop_anchor = Vector2(288, 240) +compact_anchor = Vector2(70, 310) +compact_minimum_size = Vector2(88, 84) +minimum_font_size = 12 +maximum_font_size = 16 +motion_phase = 2.45 + +[node name="WorldWtf" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +toggle_mode = true +text = "wtf" +neutral_size = Vector2(76, 72) +desktop_anchor = Vector2(225, 337) +compact_anchor = Vector2(284, 284) +compact_minimum_size = Vector2(66, 64) +minimum_font_size = 12 +maximum_font_size = 13 +motion_phase = 3.0 + +[node name="UIValue" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +mouse_filter = 2 +focus_mode = 0 +text = "ui\npixelation\nretro" +neutral_size = Vector2(160, 150) +desktop_anchor = Vector2(560, 240) +compact_anchor = Vector2(545, 210) +compact_minimum_size = Vector2(132, 126) +minimum_font_size = 15 +maximum_font_size = 25 +motion_phase = 3.55 + +[node name="UILegible" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +toggle_mode = true +text = "legible" +neutral_size = Vector2(90, 84) +desktop_anchor = Vector2(665, 175) +compact_anchor = Vector2(663, 135) +compact_minimum_size = Vector2(76, 72) +minimum_font_size = 12 +maximum_font_size = 15 +motion_phase = 4.1 + +[node name="UICute" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +toggle_mode = true +text = "cute" +neutral_size = Vector2(82, 78) +desktop_anchor = Vector2(590, 124) +compact_anchor = Vector2(575, 78) +compact_minimum_size = Vector2(70, 68) +minimum_font_size = 12 +maximum_font_size = 14 +motion_phase = 4.65 + +[node name="UIRetro" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +toggle_mode = true +button_pressed = true +text = "retro" +neutral_size = Vector2(84, 80) +desktop_anchor = Vector2(483, 149) +compact_anchor = Vector2(448, 115) +compact_minimum_size = Vector2(72, 68) +minimum_font_size = 12 +maximum_font_size = 14 +motion_phase = 5.2 + +[node name="UIHardcore" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +toggle_mode = true +text = "hardcore" +neutral_size = Vector2(100, 94) +desktop_anchor = Vector2(432, 240) +compact_anchor = Vector2(650, 310) +compact_minimum_size = Vector2(88, 84) +minimum_font_size = 12 +maximum_font_size = 16 +motion_phase = 5.75 + +[node name="UIWtf" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +toggle_mode = true +text = "wtf" +neutral_size = Vector2(76, 72) +desktop_anchor = Vector2(495, 337) +compact_anchor = Vector2(436, 284) +compact_minimum_size = Vector2(66, 64) +minimum_font_size = 12 +maximum_font_size = 13 +motion_phase = 0.55 + +[node name="DisplayBackButton" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "back" +neutral_size = Vector2(96, 90) +desktop_anchor = Vector2(350, 445) +compact_anchor = Vector2(315, 390) +compact_minimum_size = Vector2(82, 78) +minimum_font_size = 13 +maximum_font_size = 16 +motion_phase = 1.1 + +[node name="ControlsPage" type="Control" parent="."] +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 +script = ExtResource("3_page") +page_id = &"controls" +bubble_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControlsBackButton")]) +focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControlsBackButton")]) +initial_focus_path = NodePath("BubbleCluster/MouseValue") +back_focus_path = NodePath("BubbleCluster/ControlsBackButton") + +[node name="BubbleCluster" type="Control" parent="ControlsPage"] +layout_mode = 0 +offset_right = 720.0 +offset_bottom = 520.0 +script = ExtResource("5_cluster") +profile = ExtResource("6_profile") +desktop_reference_size = Vector2(720, 520) +compact_reference_size = Vector2(608, 448) + +[node name="MouseDecrease" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "−" +neutral_size = Vector2(80, 76) +desktop_anchor = Vector2(75, 140) +compact_anchor = Vector2(49, 125) +compact_minimum_size = Vector2(68, 66) +minimum_font_size = 17 +maximum_font_size = 24 +motion_phase = 0.3 + +[node name="MouseValue" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "mouse\nsensitivity\n0.0050" +neutral_size = Vector2(190, 180) +desktop_anchor = Vector2(210, 140) +compact_anchor = Vector2(200, 125) +compact_minimum_size = Vector2(160, 152) +minimum_font_size = 15 +maximum_font_size = 25 +motion_phase = 1.0 + +[node name="MouseIncrease" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "+" +neutral_size = Vector2(80, 76) +desktop_anchor = Vector2(345, 140) +compact_anchor = Vector2(351, 125) +compact_minimum_size = Vector2(68, 66) +minimum_font_size = 17 +maximum_font_size = 24 +motion_phase = 1.7 + +[node name="ControllerDecrease" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "−" +neutral_size = Vector2(80, 76) +desktop_anchor = Vector2(75, 365) +compact_anchor = Vector2(49, 340) +compact_minimum_size = Vector2(68, 66) +minimum_font_size = 17 +maximum_font_size = 24 +motion_phase = 2.4 + +[node name="ControllerValue" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "controller\nsensitivity\n2.5" +neutral_size = Vector2(190, 180) +desktop_anchor = Vector2(210, 365) +compact_anchor = Vector2(200, 340) +compact_minimum_size = Vector2(160, 152) +minimum_font_size = 15 +maximum_font_size = 25 +motion_phase = 3.1 + +[node name="ControllerIncrease" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "+" +neutral_size = Vector2(80, 76) +desktop_anchor = Vector2(345, 365) +compact_anchor = Vector2(351, 340) +compact_minimum_size = Vector2(68, 66) +minimum_font_size = 17 +maximum_font_size = 24 +motion_phase = 3.8 + +[node name="InvertYToggle" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "invert\nvertical\ncamera\noff" +neutral_size = Vector2(176, 170) +desktop_anchor = Vector2(525, 230) +compact_anchor = Vector2(485, 225) +compact_minimum_size = Vector2(170, 164) +minimum_font_size = 14 +maximum_font_size = 24 +motion_phase = 4.5 + +[node name="ControlsBackButton" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "back" +neutral_size = Vector2(100, 94) +desktop_anchor = Vector2(525, 425) +compact_anchor = Vector2(540, 395) +compact_minimum_size = Vector2(84, 80) +minimum_font_size = 14 +maximum_font_size = 17 +motion_phase = 5.2 + +[node name="AccessibilityPage" type="Control" parent="."] +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 +script = ExtResource("3_page") +page_id = &"accessibility" +bubble_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/IntervalDecrease"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/IntervalIncrease"), NodePath("BubbleCluster/IntervalHelp"), NodePath("BubbleCluster/AccessibilityBackButton")]) +focus_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/IntervalDecrease"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/IntervalIncrease"), NodePath("BubbleCluster/AccessibilityBackButton")]) +initial_focus_path = NodePath("BubbleCluster/AutoClickToggle") +back_focus_path = NodePath("BubbleCluster/AccessibilityBackButton") + +[node name="BubbleCluster" type="Control" parent="AccessibilityPage"] +layout_mode = 0 +offset_right = 720.0 +offset_bottom = 520.0 +script = ExtResource("5_cluster") +profile = ExtResource("6_profile") +desktop_reference_size = Vector2(720, 520) +compact_reference_size = Vector2(608, 448) + +[node name="AutoClickToggle" parent="AccessibilityPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "accessibility\nauto-click\noff" +neutral_size = Vector2(190, 180) +desktop_anchor = Vector2(200, 195) +compact_anchor = Vector2(129, 185) +compact_minimum_size = Vector2(160, 152) +minimum_font_size = 14 +maximum_font_size = 27 +motion_phase = 0.5 + +[node name="IntervalDecrease" parent="AccessibilityPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "−" +neutral_size = Vector2(80, 76) +desktop_anchor = Vector2(340, 210) +compact_anchor = Vector2(280, 205) +compact_minimum_size = Vector2(68, 66) +minimum_font_size = 17 +maximum_font_size = 24 +motion_phase = 1.4 + +[node name="AutoClickIntervalValue" parent="AccessibilityPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "auto-click\ninterval\n0.20 s" +neutral_size = Vector2(190, 180) +desktop_anchor = Vector2(475, 210) +compact_anchor = Vector2(431, 205) +compact_minimum_size = Vector2(160, 152) +minimum_font_size = 14 +maximum_font_size = 26 +motion_phase = 2.3 + +[node name="IntervalIncrease" parent="AccessibilityPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "+" +neutral_size = Vector2(80, 76) +desktop_anchor = Vector2(610, 210) +compact_anchor = Vector2(582, 205) +compact_minimum_size = Vector2(68, 66) +minimum_font_size = 17 +maximum_font_size = 24 +motion_phase = 3.2 + +[node name="IntervalHelp" parent="AccessibilityPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +mouse_filter = 2 +focus_mode = 0 +text = "lower intervals\nclick barriers\nfaster\nwhile held" +neutral_size = Vector2(170, 160) +desktop_anchor = Vector2(315, 405) +compact_anchor = Vector2(280, 370) +compact_minimum_size = Vector2(160, 152) +minimum_font_size = 13 +maximum_font_size = 22 +motion_phase = 4.1 + +[node name="AccessibilityBackButton" parent="AccessibilityPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "back" +neutral_size = Vector2(96, 90) +desktop_anchor = Vector2(560, 420) +compact_anchor = Vector2(520, 380) +compact_minimum_size = Vector2(82, 78) +minimum_font_size = 13 +maximum_font_size = 16 +motion_phase = 5.0 diff --git a/ui/title_screen.gd b/ui/title_screen.gd index 46839ff..510e67e 100644 --- a/ui/title_screen.gd +++ b/ui/title_screen.gd @@ -64,6 +64,13 @@ const START_PROMPT_MIN_SCALE: float = 0.985 const START_PROMPT_MAX_SCALE: float = 1.015 const START_PROMPT_CYCLE_SECONDS: float = 3.0 const DISABLED_BUBBLE_LABEL_ALPHA: float = 0.55 +const INTRO_PROMPT_FADE_DURATION: float = 0.55 +const INTRO_BUBBLE_TRAVEL_DURATION: float = 2.40 +const INTRO_BRANDING_TRAVEL_DURATION: float = 2.0 +const INTRO_BRANDING_START_DELAY: float = 0.35 +const HOST_PRESENTATION_OUT_DURATION: float = 1.70 +const HOST_PRESENTATION_IN_DURATION: float = 1.85 +const HOST_CLUSTER_SAFE_MARGIN: float = 24.0 signal gameplay_requested signal quit_requested @@ -93,7 +100,16 @@ enum ConfirmationAction { @onready var _decorative_bubble_event_timer: Timer = %DecorativeBubbleEventTimer @onready var _decorative_bubble_cluster_timer: Timer = %DecorativeBubbleClusterTimer @onready var _title_logo: TextureRect = %TitleLogo +@onready var _playtest_label: Label = %PlaytestLabel +@onready var _intro_branding_anchor: Control = %IntroBrandingAnchor +@onready var _presentation_center: CenterContainer = $Center +@onready var _main_content: VBoxContainer = $Center/MainContent +@onready var _branding_slot: Control = $Center/MainContent/BrandingSlot +@onready var _branding_motion_root: Control = %BrandingMotionRoot +@onready var _title_spacer: Control = $Center/MainContent/Spacer @onready var _button_center: CenterContainer = %ButtonCenter +@onready var _bubble_layout_slot: Control = $Center/MainContent/ButtonCenter/BubbleLayoutSlot +@onready var _bubble_motion_root: Control = %BubbleMotionRoot @onready var _bubble_field: BubbleClusterType = %BubbleField @onready var _start_prompt_center: CenterContainer = %StartPromptCenter @onready var _start_prompt_label: Label = %StartPromptLabel @@ -117,10 +133,39 @@ var _start_prompt_elapsed: float = 0.0 var _navigation_focus_active: bool = false var _modal_restore_navigation_focus: bool = false var _world_pixel_size: int = PlayerSettings.DEFAULT_WORLD_PIXEL_SIZE +var _title_settings_transition: Tween +var _title_settings_transition_generation: int = 0 +var _title_settings_transition_active: bool = false +var _title_bubble_rest_position: Vector2 = Vector2.ZERO +var _title_content_rest_position: Vector2 = Vector2.ZERO +var _title_entry_transition: Tween +var _title_entry_generation: int = 0 +var _title_entry_transition_active: bool = false +var _intro_geometry_ready: bool = false +var _intro_layout_generation: int = 0 +var _branding_intro_position: Vector2 = Vector2.ZERO +var _bubble_intro_position: Vector2 = Vector2.ZERO +var _feedback_visible_before_settings: bool = false +var _feedback_text_before_settings: String = "" +var _continue_stats_hovered: bool = false +var _continue_stats_focused: bool = false +var _continue_stats_fade: Tween func _ready() -> void: _continue_button.pressed.connect(_on_continue_pressed) + _continue_button.mouse_entered.connect( + _on_continue_stats_hover_changed.bind(true) + ) + _continue_button.mouse_exited.connect( + _on_continue_stats_hover_changed.bind(false) + ) + _continue_button.focus_entered.connect( + _on_continue_stats_focus_changed.bind(true) + ) + _continue_button.focus_exited.connect( + _on_continue_stats_focus_changed.bind(false) + ) _new_game_button.pressed.connect(_on_new_game_pressed) _settings_button.pressed.connect(_open_settings) _delete_button.pressed.connect(_on_delete_pressed) @@ -129,6 +174,7 @@ func _ready() -> void: %CancelConfirmButton.pressed.connect(_close_confirmation) _settings_panel.applied.connect(_on_settings_applied) _settings_panel.closed.connect(_on_settings_closed) + _settings_panel.opened.connect(_on_settings_opened) _bubble_field.configure(_get_title_buttons()) _decorative_fish_timer.timeout.connect(_on_decorative_fish_timer_timeout) _decorative_bubble_event_timer.timeout.connect( @@ -144,6 +190,8 @@ func _ready() -> void: set_process(false) call_deferred("_update_title_layout") call_deferred("_update_start_prompt_pivot") + call_deferred("_capture_title_bubble_rest_position") + call_deferred("_capture_title_content_rest_position") func setup( @@ -161,6 +209,8 @@ func setup( func reopen() -> void: + _cancel_title_entry_transition() + _cancel_title_settings_transition() Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _prepare_awaiting_start_input() _close_confirmation() @@ -213,6 +263,17 @@ func _update_title_layout() -> void: logo_width, logo_width / LOGO_ASPECT_RATIO ) + var branding_separation: float = float( + _main_content.get_theme_constant("separation") + ) + var branding_size := Vector2( + logo_width, + logo_width / LOGO_ASPECT_RATIO + + branding_separation + + _playtest_label.get_combined_minimum_size().y + ) + _branding_slot.custom_minimum_size = branding_size + _branding_motion_root.size = branding_size var field_width: float = minf(BUBBLE_FIELD_MAX_WIDTH, available_width) var field_height: float = ( BUBBLE_FIELD_COMPACT_HEIGHT @@ -221,16 +282,22 @@ func _update_title_layout() -> void: ) if size.y < BUBBLE_COMPACT_HEIGHT_THRESHOLD: field_width = minf(BUBBLE_FIELD_COMPACT_WIDTH, available_width) + _bubble_layout_slot.custom_minimum_size = Vector2(field_width, field_height) + _bubble_motion_root.size = Vector2(field_width, field_height) _bubble_field.custom_minimum_size = Vector2(field_width, field_height) var compact_layout: bool = field_height == BUBBLE_FIELD_COMPACT_HEIGHT _bubble_field.apply_layout( Vector2(field_width, field_height), compact_layout ) + if not _title_settings_transition_active: + call_deferred("_capture_title_bubble_rest_position") _new_game_label.text = "new\ngame" if compact_layout else "new game" _delete_save_label.text = ( "delete\nsave" if compact_layout else "delete save" ) + if _awaiting_start_input and not _title_entry_transition_active: + _schedule_intro_presentation() _update_world_preview_resolution() @@ -311,16 +378,22 @@ func _process(delta: float) -> void: pulse_weight ) _start_prompt_label.scale = Vector2.ONE * prompt_scale - if _button_center.visible: + if _main_content.visible and _button_center.visible: _bubble_field.advance_motion(delta) func _input(event: InputEvent) -> void: if not visible: return + if _title_settings_transition_active or _title_entry_transition_active: + get_viewport().set_input_as_handled() + return if _awaiting_start_input: if not _is_start_prompt_reveal_event(event): return + if not _intro_geometry_ready: + get_viewport().set_input_as_handled() + return _reveal_primary_menu() get_viewport().set_input_as_handled() return @@ -332,7 +405,7 @@ func _input(event: InputEvent) -> void: if _confirmation_panel.visible: _close_confirmation() elif _settings_panel.visible: - _close_settings() + _settings_panel.handle_back() else: return get_viewport().set_input_as_handled() @@ -359,6 +432,7 @@ func _handle_primary_menu_focus_input(event: InputEvent) -> bool: if event is InputEventMouseMotion: _navigation_focus_active = false _release_primary_menu_focus() + _update_continue_stats_visibility() return false if event is InputEventKey and (event as InputEventKey).echo: return false @@ -409,15 +483,25 @@ func _begin_title_entry() -> void: func _prepare_awaiting_start_input() -> void: + _cancel_title_entry_transition() _awaiting_start_input = true + _intro_geometry_ready = false _navigation_focus_active = false _modal_restore_navigation_focus = false - _button_center.hide() - _feedback_label.hide() + _button_center.show() + _feedback_label.show() + _feedback_label.modulate.a = 0.0 + _continue_stats_hovered = false + _continue_stats_focused = false + _cancel_continue_stats_fade() + _main_content.show() _start_prompt_center.show() + _start_prompt_center.modulate.a = 1.0 + _set_title_bubbles_interactive(false) var focus_owner: Control = get_viewport().gui_get_focus_owner() if focus_owner != null and is_ancestor_of(focus_owner): focus_owner.release_focus() + _schedule_intro_presentation() func _start_entry_prompt_animation() -> void: @@ -440,16 +524,240 @@ func _update_start_prompt_pivot() -> void: func _reveal_primary_menu() -> void: - if not _awaiting_start_input: + if ( + not _awaiting_start_input + or not _intro_geometry_ready + or _title_entry_transition_active + ): return _awaiting_start_input = false _stop_entry_prompt_animation() - _start_prompt_center.hide() - _button_center.show() - _feedback_label.show() + _title_entry_generation += 1 + _title_entry_transition_active = true + _set_title_bubbles_interactive(false) + var generation: int = _title_entry_generation set_process(true) _navigation_focus_active = false _release_title_focus() + _title_entry_transition = create_tween() + _title_entry_transition.set_parallel(true) + _title_entry_transition.tween_property( + _start_prompt_center, + "modulate:a", + 0.0, + INTRO_PROMPT_FADE_DURATION + ).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) + _title_entry_transition.tween_property( + _bubble_motion_root, + "position", + Vector2.ZERO, + INTRO_BUBBLE_TRAVEL_DURATION + ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + _title_entry_transition.tween_property( + _branding_motion_root, + "position", + Vector2.ZERO, + INTRO_BRANDING_TRAVEL_DURATION + ).set_delay(INTRO_BRANDING_START_DELAY).set_trans( + Tween.TRANS_QUAD + ).set_ease(Tween.EASE_IN_OUT) + _title_entry_transition.finished.connect( + _finish_primary_menu_reveal.bind(generation), + CONNECT_ONE_SHOT + ) + + +func _finish_primary_menu_reveal(generation: int) -> void: + if ( + generation != _title_entry_generation + or not _title_entry_transition_active + ): + return + _title_entry_transition = null + _title_entry_transition_active = false + _start_prompt_center.hide() + _start_prompt_center.modulate.a = 1.0 + _feedback_label.modulate.a = 0.0 + _set_title_bubbles_interactive(true) + + +func _cancel_title_entry_transition() -> void: + _title_entry_generation += 1 + if _title_entry_transition != null: + _title_entry_transition.kill() + _title_entry_transition = null + _title_entry_transition_active = false + if is_node_ready(): + _start_prompt_center.modulate.a = 1.0 + + +func _schedule_intro_presentation() -> void: + _intro_layout_generation += 1 + _intro_geometry_ready = false + call_deferred( + "_prepare_intro_presentation", + _intro_layout_generation + ) + + +func _prepare_intro_presentation(generation: int) -> void: + await get_tree().process_frame + await get_tree().process_frame + if ( + generation != _intro_layout_generation + or not is_node_ready() + or not _awaiting_start_input + or _title_entry_transition_active + ): + return + _branding_motion_root.size = _branding_slot.size + _bubble_motion_root.size = _bubble_layout_slot.size + var intro_tail_height: float = ( + _title_spacer.get_combined_minimum_size().y + + float(_main_content.get_theme_constant("separation")) + ) + var branding_target_global := Vector2( + floorf( + _intro_branding_anchor.global_position.x + - _branding_motion_root.size.x * 0.5 + ), + floorf( + _intro_branding_anchor.global_position.y + - (_branding_motion_root.size.y + intro_tail_height) * 0.5 + ) + ) + _branding_intro_position = ( + branding_target_global + - _branding_slot.global_position + ) + var bubble_bounds: Rect2 = _get_title_bubble_global_bounds() + bubble_bounds.position -= _bubble_motion_root.position + var presentation_allowance: float = _get_bubble_offscreen_allowance() + _bubble_intro_position = Vector2( + 0.0, + size.y + + presentation_allowance + - bubble_bounds.position.y + ) + _branding_motion_root.position = _branding_intro_position + _bubble_motion_root.position = _bubble_intro_position + _title_content_rest_position = _presentation_center.position + _title_bubble_rest_position = _bubble_field.position + _intro_geometry_ready = true + + +func _get_title_bubble_global_bounds() -> Rect2: + var bubbles: Array[BubbleButton] = _get_title_buttons() + var bounds: Rect2 = bubbles[0].get_global_rect() + for index: int in range(1, bubbles.size()): + bounds = bounds.merge(bubbles[index].get_global_rect()) + return bounds + + +func _get_bubble_offscreen_allowance() -> float: + var maximum_vertical_amplitude: float = 0.0 + var maximum_deformation_growth: float = 0.0 + for bubble: BubbleButton in _get_title_buttons(): + maximum_vertical_amplitude = maxf( + maximum_vertical_amplitude, + bubble.vertical_amplitude + ) + var maximum_scale: float = ( + 1.0 + bubble.deformation_amplitude + ) * _bubble_field.profile.hover_focus_scale + maximum_deformation_growth = maxf( + maximum_deformation_growth, + bubble.presented_size.y * (maximum_scale - 1.0) * 0.5 + ) + return ( + HOST_CLUSTER_SAFE_MARGIN + + maximum_vertical_amplitude + + maximum_deformation_growth + + _bubble_field.profile.hover_focus_lift + + _bubble_field.profile.maximum_separation + ) + + +func _on_continue_stats_hover_changed(hovered: bool) -> void: + _continue_stats_hovered = hovered + _update_continue_stats_visibility() + + +func _on_continue_stats_focus_changed(focused: bool) -> void: + _continue_stats_focused = focused + _update_continue_stats_visibility() + + +func _update_continue_stats_visibility() -> void: + if not is_node_ready(): + return + var requested_visible: bool = ( + _continue_stats_hovered + or ( + _continue_stats_focused + and _navigation_focus_active + ) + ) + requested_visible = ( + requested_visible + and not _continue_button.disabled + and _inspection != null + and _inspection.status == SaveInspectionType.Status.VALID_SUPPORTED + and not _awaiting_start_input + and not _title_entry_transition_active + and not _title_settings_transition_active + and not _settings_panel.visible + and not _confirmation_panel.visible + and not _action_in_progress + and _presentation_center.visible + ) + if requested_visible: + _feedback_label.text = _get_continue_stats_text() + _fade_continue_stats_to(1.0 if requested_visible else 0.0) + + +func _hide_continue_stats_context() -> void: + _continue_stats_hovered = false + _continue_stats_focused = false + _fade_continue_stats_to(0.0) + + +func _fade_continue_stats_to(target_opacity: float) -> void: + if not is_node_ready(): + return + _cancel_continue_stats_fade() + if is_equal_approx(_feedback_label.modulate.a, target_opacity): + _feedback_label.modulate.a = target_opacity + return + _continue_stats_fade = create_tween() + _continue_stats_fade.tween_property( + _feedback_label, + "modulate:a", + target_opacity, + INTRO_PROMPT_FADE_DURATION + ).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) + + +func _cancel_continue_stats_fade() -> void: + if _continue_stats_fade != null: + _continue_stats_fade.kill() + _continue_stats_fade = null + + +func _get_continue_stats_text() -> String: + if ( + _inspection == null + or _inspection.status != SaveInspectionType.Status.VALID_SUPPORTED + ): + return "" + return ( + "%d fish • $%d • %d discovered" + % [ + _inspection.catch_count, + _inspection.wallet_balance, + _inspection.discovered_species_count, + ] + ) func _on_continue_pressed() -> void: @@ -461,6 +769,7 @@ func _on_continue_pressed() -> void: or not _inspection.can_continue() ): return + _hide_continue_stats_context() _action_in_progress = true if _save_manager.load_player_data(): _feedback_label.text = "save loaded." @@ -479,6 +788,7 @@ func _on_new_game_pressed() -> void: or _inspection == null ): return + _hide_continue_stats_context() if _inspection.status == SaveInspectionType.Status.MISSING: if _save_manager.initialize_new_game(): gameplay_requested.emit() @@ -508,6 +818,7 @@ func _on_delete_pressed() -> void: or not _inspection.can_delete() ): return + _hide_continue_stats_context() _open_confirmation( ConfirmationAction.DELETE_SAVE, "delete your saved progression? this cannot be undone.", @@ -540,6 +851,7 @@ func _open_confirmation( message: String, confirm_text: String, ) -> void: + _hide_continue_stats_context() _modal_restore_navigation_focus = _navigation_focus_active _confirmation_action = action _confirmation_text.text = message @@ -563,11 +875,18 @@ func _close_confirmation() -> void: func _open_settings() -> void: - if _confirmation_panel.visible or _action_in_progress: + if ( + _confirmation_panel.visible + or _action_in_progress + or _settings_panel.visible + or _title_settings_transition_active + ): return + _hide_continue_stats_context() _modal_restore_navigation_focus = _navigation_focus_active - _feedback_label.text = "" - _settings_panel.open_panel(_settings_manager) + _feedback_visible_before_settings = _feedback_label.visible + _feedback_text_before_settings = _feedback_label.text + _begin_title_cluster_exit() func _close_settings() -> void: @@ -575,12 +894,158 @@ func _close_settings() -> void: func _on_settings_applied() -> void: - _feedback_label.text = "settings saved." - _restore_settings_focus() + _begin_title_cluster_return("settings saved.") func _on_settings_closed() -> void: + _begin_title_cluster_return(_feedback_text_before_settings) + + +func _on_settings_opened() -> void: + _title_settings_transition_active = false + + +func _begin_title_cluster_exit() -> void: + _title_settings_transition_generation += 1 + _cancel_title_settings_tween() + _title_settings_transition_active = true + _title_content_rest_position = _presentation_center.position + _title_bubble_rest_position = _bubble_field.position + _set_title_bubbles_interactive(false) + _release_primary_menu_focus() + var generation: int = _title_settings_transition_generation + var exit_distance: float = ( + _main_content.global_position.y + + _main_content.size.y + + HOST_CLUSTER_SAFE_MARGIN + ) + _title_settings_transition = create_tween() + _title_settings_transition.tween_property( + _presentation_center, + "position:y", + _title_content_rest_position.y - exit_distance, + HOST_PRESENTATION_OUT_DURATION + ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + _title_settings_transition.finished.connect( + _finish_title_cluster_exit.bind(generation), + CONNECT_ONE_SHOT + ) + + +func _finish_title_cluster_exit(generation: int) -> void: + if ( + generation != _title_settings_transition_generation + or not _title_settings_transition_active + ): + return + _title_settings_transition = null + _presentation_center.hide() + _presentation_center.position = _title_content_rest_position + _settings_panel.open_panel( + _settings_manager, + SettingsPanelType.PresentationMode.TITLE_EMBEDDED + ) + + +func _begin_title_cluster_return(feedback_text: String) -> void: + _title_settings_transition_generation += 1 + _cancel_title_settings_tween() + _title_settings_transition_active = true + if _feedback_label.text != feedback_text: + _feedback_label.text = feedback_text + _feedback_label.visible = _feedback_visible_before_settings + _hide_continue_stats_context() + _set_title_bubbles_interactive(false) + var generation: int = _title_settings_transition_generation + var rest_global_y: float = ( + _presentation_center.global_position.y + - _presentation_center.position.y + + _title_content_rest_position.y + ) + _presentation_center.position = Vector2( + _title_content_rest_position.x, + _title_content_rest_position.y + + size.y + + HOST_CLUSTER_SAFE_MARGIN + - rest_global_y + ) + _presentation_center.show() + _title_settings_transition = create_tween() + _title_settings_transition.tween_property( + _presentation_center, + "position", + _title_content_rest_position, + HOST_PRESENTATION_IN_DURATION + ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + _title_settings_transition.finished.connect( + _finish_title_cluster_return.bind(generation), + CONNECT_ONE_SHOT + ) + + +func _finish_title_cluster_return(generation: int) -> void: + if ( + generation != _title_settings_transition_generation + or not _title_settings_transition_active + ): + return + _title_settings_transition = null + _title_settings_transition_active = false + _presentation_center.position = _title_content_rest_position + _bubble_field.position = _title_bubble_rest_position + _set_title_bubbles_interactive(true) _restore_settings_focus() + _update_continue_stats_visibility() + + +func _set_title_bubbles_interactive(interactive: bool) -> void: + for bubble: BubbleButton in _get_title_buttons(): + bubble.focus_mode = ( + Control.FOCUS_ALL if interactive else Control.FOCUS_NONE + ) + bubble.mouse_filter = ( + Control.MOUSE_FILTER_STOP + if interactive + else Control.MOUSE_FILTER_IGNORE + ) + + +func _capture_title_bubble_rest_position() -> void: + if ( + not is_node_ready() + or _title_settings_transition_active + or not _button_center.visible + ): + return + _title_bubble_rest_position = _bubble_field.position + + +func _capture_title_content_rest_position() -> void: + if ( + not is_node_ready() + or _title_settings_transition_active + or _title_entry_transition_active + or not _button_center.visible + ): + return + _title_content_rest_position = _presentation_center.position + + +func _cancel_title_settings_transition() -> void: + _title_settings_transition_generation += 1 + _cancel_title_settings_tween() + _title_settings_transition_active = false + if is_node_ready(): + _presentation_center.position = _title_content_rest_position + _presentation_center.show() + _bubble_field.position = _title_bubble_rest_position + _set_title_bubbles_interactive(true) + + +func _cancel_title_settings_tween() -> void: + if _title_settings_transition != null: + _title_settings_transition.kill() + _title_settings_transition = null func _restore_settings_focus() -> void: @@ -603,14 +1068,11 @@ func _refresh_save_inspection() -> void: ) _feedback_label.text = _inspection.message if _inspection.status == SaveInspectionType.Status.VALID_SUPPORTED: - _feedback_label.text = ( - "%d fish • $%d • %d discovered" - % [ - _inspection.catch_count, - _inspection.wallet_balance, - _inspection.discovered_species_count, - ] - ) + _feedback_label.text = _get_continue_stats_text() + if _continue_button.disabled: + _hide_continue_stats_context() + else: + _update_continue_stats_visibility() func _focus_initial_button() -> void: @@ -626,6 +1088,7 @@ func _focus_initial_button() -> void: else: _new_game_button.grab_focus() _navigation_focus_active = true + _update_continue_stats_visibility() func _on_quit_pressed() -> void: @@ -634,6 +1097,7 @@ func _on_quit_pressed() -> void: and not _confirmation_panel.visible and not _settings_panel.visible ): + _hide_continue_stats_context() quit_requested.emit() @@ -644,6 +1108,7 @@ func _on_title_visibility_changed() -> void: set_process(true) _start_decorative_presentation() else: + _hide_continue_stats_context() _stop_entry_prompt_animation() _navigation_focus_active = false _modal_restore_navigation_focus = false diff --git a/ui/title_screen.tscn b/ui/title_screen.tscn index ac5e1ef..ca6b8df 100644 --- a/ui/title_screen.tscn +++ b/ui/title_screen.tscn @@ -101,6 +101,17 @@ text = "press any key to start" horizontal_alignment = 1 vertical_alignment = 1 +[node name="IntroBrandingAnchor" type="Control" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + [node name="Center" type="CenterContainer" parent="."] z_index = 10 layout_mode = 1 @@ -115,7 +126,27 @@ layout_mode = 2 theme_override_constants/separation = 7 alignment = 1 -[node name="TitleLogo" type="TextureRect" parent="Center/MainContent"] +[node name="BrandingSlot" type="Control" parent="Center/MainContent"] +custom_minimum_size = Vector2(640, 215) +layout_mode = 2 + +[node name="BrandingMotionRoot" type="Control" parent="Center/MainContent/BrandingSlot"] +unique_name_in_owner = true +layout_mode = 0 +offset_right = 640.0 +offset_bottom = 215.0 +mouse_filter = 2 + +[node name="BrandingContent" type="VBoxContainer" parent="Center/MainContent/BrandingSlot/BrandingMotionRoot"] +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="TitleLogo" type="TextureRect" parent="Center/MainContent/BrandingSlot/BrandingMotionRoot/BrandingContent"] unique_name_in_owner = true custom_minimum_size = Vector2(640, 190) material = SubResource("ShaderMaterial_title_logo") @@ -126,7 +157,8 @@ texture = ExtResource("5_logo") expand_mode = 1 stretch_mode = 5 -[node name="PlaytestLabel" type="Label" parent="Center/MainContent"] +[node name="PlaytestLabel" type="Label" parent="Center/MainContent/BrandingSlot/BrandingMotionRoot/BrandingContent"] +unique_name_in_owner = true layout_mode = 2 theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1) theme_override_font_sizes/font_size = 17 @@ -142,14 +174,27 @@ unique_name_in_owner = true visible = false layout_mode = 2 -[node name="BubbleField" type="Control" parent="Center/MainContent/ButtonCenter"] -unique_name_in_owner = true +[node name="BubbleLayoutSlot" type="Control" parent="Center/MainContent/ButtonCenter"] custom_minimum_size = Vector2(396, 318) layout_mode = 2 + +[node name="BubbleMotionRoot" type="Control" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot"] +unique_name_in_owner = true +layout_mode = 0 +offset_right = 396.0 +offset_bottom = 318.0 +mouse_filter = 2 + +[node name="BubbleField" type="Control" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot"] +unique_name_in_owner = true +custom_minimum_size = Vector2(396, 318) +layout_mode = 0 +offset_right = 396.0 +offset_bottom = 318.0 script = ExtResource("8_bubble_cluster") profile = ExtResource("9_bubble_profile") -[node name="ContinueButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleField"] +[node name="ContinueButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true offset_left = 123.0 offset_top = 86.0 @@ -169,7 +214,7 @@ motion_period = 5.8 deformation_amplitude = 0.01 deformation_period = 6.7 -[node name="NewGameButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleField"] +[node name="NewGameButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true offset_left = 42.0 offset_top = 4.0 @@ -191,7 +236,7 @@ motion_phase = 1.2 deformation_amplitude = 0.018 deformation_period = 5.9 -[node name="NewGameLabel" type="Label" parent="Center/MainContent/ButtonCenter/BubbleField/NewGameButton"] +[node name="NewGameLabel" type="Label" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField/NewGameButton"] unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 @@ -206,7 +251,7 @@ text = "new game" horizontal_alignment = 1 vertical_alignment = 1 -[node name="SettingsButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleField"] +[node name="SettingsButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true offset_left = 264.0 offset_top = 17.0 @@ -225,7 +270,7 @@ motion_period = 4.9 motion_phase = 2.5 deformation_period = 6.3 -[node name="DeleteSaveButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleField"] +[node name="DeleteSaveButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true offset_left = -11.0 offset_top = 122.0 @@ -246,7 +291,7 @@ motion_phase = 3.7 deformation_amplitude = 0.018 deformation_period = 5.4 -[node name="DeleteSaveLabel" type="Label" parent="Center/MainContent/ButtonCenter/BubbleField/DeleteSaveButton"] +[node name="DeleteSaveLabel" type="Label" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField/DeleteSaveButton"] unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 @@ -261,7 +306,7 @@ text = "delete save" horizontal_alignment = 1 vertical_alignment = 1 -[node name="QuitButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleField"] +[node name="QuitButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true offset_left = 275.0 offset_top = 217.0