From f04d8db8ac4bc25b56759d3429b433d63a6714a6 Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 27 Jul 2026 21:11:25 -0400 Subject: [PATCH] Expand bubble menu presentation --- ui/game_ui.gd | 2 +- ui/pixelation_reset_overlay.gd | 99 +++++- ui/pixelation_reset_overlay.tscn | 108 +++++- ui/settings/settings_bubble_page.gd | 4 +- ui/settings_panel.gd | 3 + ui/title_confirmation_bubble_page.gd | 252 +++++++++++++ ui/title_confirmation_bubble_page.gd.uid | 1 + ui/title_confirmation_bubble_page.tscn | 129 +++++++ ui/title_screen.gd | 435 +++++++++++++++++++---- ui/title_screen.tscn | 148 ++++---- 10 files changed, 1016 insertions(+), 165 deletions(-) create mode 100644 ui/title_confirmation_bubble_page.gd create mode 100644 ui/title_confirmation_bubble_page.gd.uid create mode 100644 ui/title_confirmation_bubble_page.tscn diff --git a/ui/game_ui.gd b/ui/game_ui.gd index d32184a..2a71c0a 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -53,7 +53,7 @@ signal interactive_pointer_ui_changed(is_open: bool) @onready var _shop_prompt: PanelContainer = %ShopPrompt @onready var _effect_status: Label = %EffectStatus @onready var _title_settings_panel: SettingsPanelType = ( - $UIRoot/TitleScreen/SettingsPanel + $UIRoot/TitleScreen/ResponsiveTitleStage/TitlePresentationScaleRoot/SettingsPanel ) @onready var _pause_settings_panel: SettingsPanelType = ( $UIRoot/PauseMenu/SettingsCenter/SettingsPanel diff --git a/ui/pixelation_reset_overlay.gd b/ui/pixelation_reset_overlay.gd index 8aea79a..9f67878 100644 --- a/ui/pixelation_reset_overlay.gd +++ b/ui/pixelation_reset_overlay.gd @@ -1,25 +1,64 @@ class_name PixelationResetOverlay extends CanvasLayer +const DISPLAY_REFERENCE_SIZE: Vector2 = Vector2(1280.0, 720.0) +const MINIMUM_PRESENTATION_SCALE: float = 0.90 +const MAXIMUM_PRESENTATION_SCALE: float = 1.35 +const LARGE_DISPLAY_GROWTH: float = 0.45 +const SCREEN_MARGIN: float = 16.0 +const FADE_DURATION: float = 0.55 + signal reset_requested signal return_to_settings_requested +@onready var _presentation_root: Control = %ResetPresentationRoot @onready var _reset_button: Button = %ResetPixelationButton +var _fade_tween: Tween +var _fade_generation: int = 0 +var _settings_open: bool = false + func _ready() -> void: _reset_button.pressed.connect(reset_requested.emit) _reset_button.gui_input.connect(_on_reset_button_gui_input) + get_viewport().size_changed.connect(_update_responsive_layout) + _presentation_root.modulate.a = 0.0 + _set_interactive(false) + _update_responsive_layout() func set_settings_open(is_open: bool) -> void: - visible = is_open - if not is_open and _reset_button.has_focus(): - _reset_button.release_focus() + _settings_open = is_open + _fade_generation += 1 + if _fade_tween != null: + _fade_tween.kill() + _fade_tween = null + if is_open: + show() + _set_interactive(true) + else: + _set_interactive(false) + var target_alpha: float = 1.0 if is_open else 0.0 + if is_equal_approx(_presentation_root.modulate.a, target_alpha): + _finish_fade(_fade_generation, is_open) + return + var generation: int = _fade_generation + _fade_tween = create_tween() + _fade_tween.tween_property( + _presentation_root, + "modulate:a", + target_alpha, + FADE_DURATION + ).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) + _fade_tween.finished.connect( + _finish_fade.bind(generation, is_open), + CONNECT_ONE_SHOT + ) func focus_reset_button() -> void: - if visible: + if visible and _settings_open: _reset_button.grab_focus() @@ -30,3 +69,55 @@ func _on_reset_button_gui_input(event: InputEvent) -> void: ): return_to_settings_requested.emit() get_viewport().set_input_as_handled() + + +func _update_responsive_layout() -> void: + if not is_node_ready(): + return + var display_size: Vector2 = Vector2(get_window().size) + if display_size.x <= 1.0 or display_size.y <= 1.0: + return + var responsive_scale: float = minf( + display_size.x / DISPLAY_REFERENCE_SIZE.x, + display_size.y / DISPLAY_REFERENCE_SIZE.y + ) + var presentation_scale: float + if responsive_scale < 1.0: + presentation_scale = lerpf( + MINIMUM_PRESENTATION_SCALE, + 1.0, + clampf((responsive_scale - 0.5) / 0.5, 0.0, 1.0) + ) + else: + presentation_scale = minf( + MAXIMUM_PRESENTATION_SCALE, + 1.0 + (responsive_scale - 1.0) * LARGE_DISPLAY_GROWTH + ) + _presentation_root.scale = Vector2.ONE * presentation_scale + _presentation_root.position = ( + display_size + - _presentation_root.size * presentation_scale + - Vector2.ONE * SCREEN_MARGIN + ) + + +func _set_interactive(interactive: bool) -> void: + if not interactive and _reset_button.has_focus(): + _reset_button.release_focus() + _reset_button.focus_mode = ( + Control.FOCUS_ALL if interactive else Control.FOCUS_NONE + ) + _reset_button.mouse_filter = ( + Control.MOUSE_FILTER_STOP + if interactive + else Control.MOUSE_FILTER_IGNORE + ) + + +func _finish_fade(generation: int, faded_in: bool) -> void: + if generation != _fade_generation or faded_in != _settings_open: + return + _fade_tween = null + _presentation_root.modulate.a = 1.0 if faded_in else 0.0 + if not faded_in: + hide() diff --git a/ui/pixelation_reset_overlay.tscn b/ui/pixelation_reset_overlay.tscn index cfb1b99..0ffef5a 100644 --- a/ui/pixelation_reset_overlay.tscn +++ b/ui/pixelation_reset_overlay.tscn @@ -1,30 +1,104 @@ -[gd_scene load_steps=3 format=3] +[gd_scene load_steps=7 format=3] [ext_resource type="Script" path="res://ui/pixelation_reset_overlay.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] +[sub_resource type="StyleBoxFlat" id="StyleBox_reset_normal"] +content_margin_left = 18.0 +content_margin_top = 18.0 +content_margin_right = 18.0 +content_margin_bottom = 18.0 +bg_color = Color(0.035, 0.145, 0.22, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.91, 0.98, 1, 0.85) +corner_radius_top_left = 74 +corner_radius_top_right = 74 +corner_radius_bottom_right = 74 +corner_radius_bottom_left = 74 + +[sub_resource type="StyleBoxFlat" id="StyleBox_reset_hover"] +content_margin_left = 18.0 +content_margin_top = 18.0 +content_margin_right = 18.0 +content_margin_bottom = 18.0 +bg_color = Color(0.055, 0.205, 0.29, 1) +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.91, 0.975, 0.985, 1) +corner_radius_top_left = 74 +corner_radius_top_right = 74 +corner_radius_bottom_right = 74 +corner_radius_bottom_left = 74 + +[sub_resource type="StyleBoxFlat" id="StyleBox_reset_pressed"] +content_margin_left = 18.0 +content_margin_top = 18.0 +content_margin_right = 18.0 +content_margin_bottom = 18.0 +bg_color = Color(0.025, 0.12, 0.19, 1) +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.94, 0.99, 1, 1) +corner_radius_top_left = 74 +corner_radius_top_right = 74 +corner_radius_bottom_right = 74 +corner_radius_bottom_left = 74 + +[sub_resource type="StyleBoxFlat" id="StyleBox_reset_disabled"] +content_margin_left = 18.0 +content_margin_top = 18.0 +content_margin_right = 18.0 +content_margin_bottom = 18.0 +bg_color = Color(0.035, 0.145, 0.22, 0.72) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.81, 0.9, 0.92, 0.55) +corner_radius_top_left = 74 +corner_radius_top_right = 74 +corner_radius_bottom_right = 74 +corner_radius_bottom_left = 74 + [node name="PixelationResetOverlay" type="CanvasLayer"] layer = 300 visible = false script = ExtResource("1_script") -[node name="ResetAnchor" type="MarginContainer" parent="."] -anchors_preset = 3 -anchor_left = 1.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = -324.0 -offset_top = -86.0 -offset_right = -16.0 -offset_bottom = -16.0 -grow_horizontal = 0 -grow_vertical = 0 +[node name="ResetPresentationRoot" type="Control" parent="."] +unique_name_in_owner = true +offset_right = 156.0 +offset_bottom = 148.0 mouse_filter = 2 theme = ExtResource("2_theme") -[node name="ResetPixelationButton" type="Button" parent="ResetAnchor"] +[node name="ResetPixelationButton" type="Button" parent="ResetPresentationRoot"] unique_name_in_owner = true -custom_minimum_size = Vector2(308, 70) -layout_mode = 2 -text = "reset pixelation" +custom_minimum_size = Vector2(156, 148) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +tooltip_text = "reset pixelation" +accessibility_name = "reset pixelation" +theme_override_colors/font_color = Color(0.82, 0.93, 0.96, 1) +theme_override_colors/font_hover_color = Color(0.91, 0.975, 0.985, 1) +theme_override_colors/font_focus_color = Color(0.91, 0.975, 0.985, 1) +theme_override_colors/font_pressed_color = Color(0.7, 0.87, 0.92, 1) +theme_override_colors/font_disabled_color = Color(0.66, 0.77, 0.8, 0.78) +theme_override_font_sizes/font_size = 26 +theme_override_styles/normal = SubResource("StyleBox_reset_normal") +theme_override_styles/hover = SubResource("StyleBox_reset_hover") +theme_override_styles/pressed = SubResource("StyleBox_reset_pressed") +theme_override_styles/focus = SubResource("StyleBox_reset_hover") +theme_override_styles/disabled = SubResource("StyleBox_reset_disabled") +text = "reset\npixelation" diff --git a/ui/settings/settings_bubble_page.gd b/ui/settings/settings_bubble_page.gd index 57cea81..3483f32 100644 --- a/ui/settings/settings_bubble_page.gd +++ b/ui/settings/settings_bubble_page.gd @@ -11,8 +11,8 @@ extends Control @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.1, 2.0, 0.01) var outgoing_rise_duration: float = 1.70 +@export_range(0.1, 2.0, 0.01) var incoming_rise_duration: float = 1.85 @export_range(0.0, 128.0, 1.0) var transition_safe_margin: float = 24.0 var _cluster: BubbleCluster diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index 8c62588..850db9d 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -25,6 +25,7 @@ signal closed signal opened signal crisp_reset_focus_requested signal panel_visibility_changed(is_visible: bool) +signal navigation_transition_started enum PresentationMode { TITLE_EMBEDDED, @@ -226,6 +227,7 @@ func _start_page_transition(page_id: StringName, push_page: bool) -> void: return _page_transition_generation += 1 _page_transition_active = true + navigation_transition_started.emit() var generation: int = _page_transition_generation outgoing_page.transition_out( _finish_outgoing_page.bind( @@ -277,6 +279,7 @@ func _begin_embedded_close(applied_result: bool) -> void: return _page_transition_generation += 1 _page_transition_active = true + navigation_transition_started.emit() var generation: int = _page_transition_generation active_page.transition_out( _finish_embedded_close.bind(generation, applied_result), diff --git a/ui/title_confirmation_bubble_page.gd b/ui/title_confirmation_bubble_page.gd new file mode 100644 index 0000000..88ef4a1 --- /dev/null +++ b/ui/title_confirmation_bubble_page.gd @@ -0,0 +1,252 @@ +class_name TitleConfirmationBubblePage +extends Control + +signal confirmed +signal cancelled + +@export var cluster_path: NodePath = ^"BubbleCluster" +@export var prompt_path: NodePath = ^"BubbleCluster/PromptBubble" +@export var prompt_label_path: NodePath = ^"BubbleCluster/PromptBubble/PromptLabel" +@export var confirm_path: NodePath = ^"BubbleCluster/ConfirmButton" +@export var confirm_label_path: NodePath = ( + ^"BubbleCluster/ConfirmButton/ConfirmLabel" +) +@export var cancel_path: NodePath = ^"BubbleCluster/CancelButton" +@export var multiline_confirm_neutral_size: Vector2 = Vector2(116.0, 116.0) +@export var multiline_confirm_compact_minimum_size: Vector2 = ( + Vector2(90.0, 90.0) +) +@export var compact_height_threshold: float = 250.0 +@export_range(0.0, 128.0, 1.0) var transition_safe_margin: float = 24.0 + +var _cluster: BubbleCluster +var _prompt: BubbleButton +var _prompt_label: Label +var _confirm_button: BubbleButton +var _confirm_label: Label +var _cancel_button: BubbleButton +var _default_confirm_neutral_size: Vector2 +var _default_confirm_compact_minimum_size: Vector2 +var _bubbles: Array[BubbleButton] = [] +var _resting_cluster_position: Vector2 = Vector2.ZERO +var _transition: Tween +var _transition_generation: int = 0 +var _is_transitioning: bool = false + + +func _ready() -> void: + _cluster = get_node(cluster_path) as BubbleCluster + _prompt = get_node(prompt_path) as BubbleButton + _prompt_label = get_node(prompt_label_path) as Label + _confirm_button = get_node(confirm_path) as BubbleButton + _confirm_label = get_node(confirm_label_path) as Label + _cancel_button = get_node(cancel_path) as BubbleButton + _default_confirm_neutral_size = _confirm_button.neutral_size + _default_confirm_compact_minimum_size = ( + _confirm_button.compact_minimum_size + ) + _bubbles = [_prompt, _confirm_button, _cancel_button] + _cluster.configure(_bubbles) + _prompt.focus_mode = Control.FOCUS_NONE + _prompt.mouse_filter = Control.MOUSE_FILTER_IGNORE + _confirm_button.pressed.connect(confirmed.emit) + _cancel_button.pressed.connect(cancelled.emit) + _configure_focus() + hide_page() + + +func configure( + prompt_text: String, + confirm_text: String, + use_multiline_action_layout: bool = false, +) -> void: + _prompt_label.text = prompt_text + if use_multiline_action_layout: + _confirm_button.text = "" + _confirm_label.text = confirm_text + _confirm_label.show() + _confirm_button.label_control_path = ^"ConfirmLabel" + else: + _confirm_label.text = "" + _confirm_label.hide() + _confirm_button.label_control_path = NodePath() + _confirm_button.text = confirm_text + _confirm_button.apply_profile() + _confirm_button.neutral_size = ( + multiline_confirm_neutral_size + if use_multiline_action_layout + else _default_confirm_neutral_size + ) + _confirm_button.compact_minimum_size = ( + multiline_confirm_compact_minimum_size + if use_multiline_action_layout + else _default_confirm_compact_minimum_size + ) + + +func set_stage_rect(stage_rect: Rect2) -> void: + if not is_node_ready(): + return + var field_size := Vector2( + maxf(1.0, stage_rect.size.x), + maxf(1.0, stage_rect.size.y) + ) + _resting_cluster_position = stage_rect.position + _cluster.size = field_size + _cluster.apply_layout( + field_size, + field_size.y < compact_height_threshold + ) + if not _is_transitioning: + _cluster.position = _resting_cluster_position + + +func transition_in( + duration: float, + completed: Callable, +) -> void: + _transition_generation += 1 + _cancel_transition() + show() + _is_transitioning = true + _set_interactive(false) + set_process(true) + _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, completed), + CONNECT_ONE_SHOT + ) + + +func transition_out( + duration: float, + completed: Callable, +) -> void: + if not visible or _is_transitioning: + return + _transition_generation += 1 + _cancel_transition() + _is_transitioning = true + _set_interactive(false) + 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 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 lock_interaction() -> void: + _set_interactive(false) + + +func is_transitioning() -> bool: + return _is_transitioning + + +func focus_confirm() -> void: + if ( + _confirm_button.visible + and _confirm_button.focus_mode != Control.FOCUS_NONE + ): + _confirm_button.grab_focus() + + +func _process(delta: float) -> void: + if visible: + _cluster.advance_motion(delta) + + +func _configure_focus() -> void: + _confirm_button.focus_neighbor_left = _confirm_button.get_path_to( + _cancel_button + ) + _confirm_button.focus_neighbor_right = _confirm_button.focus_neighbor_left + _confirm_button.focus_neighbor_top = _confirm_button.focus_neighbor_left + _confirm_button.focus_neighbor_bottom = _confirm_button.focus_neighbor_left + _cancel_button.focus_neighbor_left = _cancel_button.get_path_to( + _confirm_button + ) + _cancel_button.focus_neighbor_right = _cancel_button.focus_neighbor_left + _cancel_button.focus_neighbor_top = _cancel_button.focus_neighbor_left + _cancel_button.focus_neighbor_bottom = _cancel_button.focus_neighbor_left + + +func _set_interactive(interactive: bool) -> void: + mouse_filter = ( + Control.MOUSE_FILTER_PASS + if interactive + else Control.MOUSE_FILTER_IGNORE + ) + for bubble: BubbleButton in [_confirm_button, _cancel_button]: + if bubble == null: + continue + 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_in( + generation: int, + completed: Callable, +) -> void: + if generation != _transition_generation or not visible: + return + _transition = null + _is_transitioning = false + _set_interactive(true) + focus_confirm() + completed.call() + + +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 + set_process(false) + completed.call() + + +func _cancel_transition() -> void: + if _transition != null: + _transition.kill() + _transition = null diff --git a/ui/title_confirmation_bubble_page.gd.uid b/ui/title_confirmation_bubble_page.gd.uid new file mode 100644 index 0000000..d8c6899 --- /dev/null +++ b/ui/title_confirmation_bubble_page.gd.uid @@ -0,0 +1 @@ +uid://cvxkxoqfvkp3h diff --git a/ui/title_confirmation_bubble_page.tscn b/ui/title_confirmation_bubble_page.tscn new file mode 100644 index 0000000..d6921f3 --- /dev/null +++ b/ui/title_confirmation_bubble_page.tscn @@ -0,0 +1,129 @@ +[gd_scene load_steps=6 format=3] + +[ext_resource type="Script" path="res://ui/title_confirmation_bubble_page.gd" id="1_page"] +[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_button.tscn" id="2_bubble"] +[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_cluster.gd" id="3_cluster"] +[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_menu_profile.gd" id="4_profile_script"] +[ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="5_profile"] + +[sub_resource type="Resource" id="BubbleMenuProfile_confirmation_action"] +script = ExtResource("4_profile_script") +normal_border_width = 3 +emphasized_border_width = 4 + +[node name="TitleConfirmationBubblePage" type="Control"] +visible = false +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1_page") + +[node name="BubbleCluster" type="Control" parent="."] +layout_mode = 0 +offset_right = 396.0 +offset_bottom = 318.0 +script = ExtResource("3_cluster") +profile = ExtResource("5_profile") +desktop_reference_size = Vector2(396, 318) +compact_reference_size = Vector2(294, 200) + +[node name="PromptBubble" parent="BubbleCluster" instance=ExtResource("2_bubble")] +custom_minimum_size = Vector2(0, 0) +offset_left = 70.0 +offset_top = -2.0 +offset_right = 326.0 +offset_bottom = 254.0 +neutral_size = Vector2(256, 256) +desktop_anchor = Vector2(198, 126) +compact_anchor = Vector2(198, 119) +compact_minimum_size = Vector2(190, 190) +label_control_path = NodePath("PromptLabel") +minimum_font_size = 15 +maximum_font_size = 26 +horizontal_amplitude = 1.6 +vertical_amplitude = 4.2 +motion_period = 6.4 +motion_phase = 0.45 +deformation_amplitude = 0.012 +deformation_period = 7.1 + +[node name="PromptLabel" type="Label" parent="BubbleCluster/PromptBubble"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 22.0 +offset_top = 22.0 +offset_right = -22.0 +offset_bottom = -22.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) +theme_override_constants/line_spacing = -2 +text = "confirm?" +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 2 + +[node name="ConfirmButton" parent="BubbleCluster" instance=ExtResource("2_bubble")] +custom_minimum_size = Vector2(0, 0) +offset_left = 20.0 +offset_top = 222.0 +offset_right = 136.0 +offset_bottom = 334.0 +profile = SubResource("BubbleMenuProfile_confirmation_action") +neutral_size = Vector2(116, 112) +desktop_anchor = Vector2(78, 278) +compact_anchor = Vector2(34, 280) +compact_minimum_size = Vector2(88, 86) +label_control_path = NodePath("ConfirmLabel") +minimum_font_size = 14 +maximum_font_size = 21 +horizontal_amplitude = 2.0 +vertical_amplitude = 4.8 +motion_period = 5.6 +motion_phase = 2.15 +deformation_amplitude = 0.016 +deformation_period = 5.9 + +[node name="ConfirmLabel" type="Label" parent="BubbleCluster/ConfirmButton"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 12.0 +offset_top = 12.0 +offset_right = -12.0 +offset_bottom = -12.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +text = "confirm" +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 2 + +[node name="CancelButton" parent="BubbleCluster" instance=ExtResource("2_bubble")] +custom_minimum_size = Vector2(0, 0) +offset_left = 262.0 +offset_top = 216.0 +offset_right = 374.0 +offset_bottom = 324.0 +text = "cancel" +neutral_size = Vector2(112, 108) +desktop_anchor = Vector2(318, 270) +compact_anchor = Vector2(362, 272) +compact_minimum_size = Vector2(86, 84) +minimum_font_size = 14 +maximum_font_size = 20 +horizontal_amplitude = 1.7 +vertical_amplitude = 4.1 +motion_period = 5.1 +motion_phase = 4.0 +deformation_amplitude = 0.017 +deformation_period = 5.4 diff --git a/ui/title_screen.gd b/ui/title_screen.gd index 510e67e..c3552fa 100644 --- a/ui/title_screen.gd +++ b/ui/title_screen.gd @@ -13,6 +13,9 @@ const BubbleButtonType = preload( const BubbleClusterType = preload( "res://ui/components/bubble_menu/bubble_cluster.gd" ) +const TitleConfirmationBubblePageType = preload( + "res://ui/title_confirmation_bubble_page.gd" +) const DECORATIVE_FISH_TEXTURES: Array[Texture2D] = [ preload("res://fish/species/bass/fish_bass_striped.png"), preload("res://fish/species/bluegill/fish_bluegill.png"), @@ -50,11 +53,20 @@ const BUBBLE_WOBBLE_MIN: float = 3.0 const BUBBLE_WOBBLE_MAX: float = 9.0 const BUBBLE_EDGE_MARGIN: float = 16.0 const MAX_DECORATIVE_BUBBLES: int = 6 +const TRANSITION_BUBBLE_COUNT_MIN: int = 4 +const TRANSITION_BUBBLE_COUNT_MAX: int = 5 +const MAX_TRANSITION_BUBBLES: int = 30 +const TRANSITION_BUBBLE_X_MIN: float = 0.36 +const TRANSITION_BUBBLE_X_MAX: float = 0.64 +const TRANSITION_BUBBLE_Y_MIN: float = 0.60 +const TRANSITION_BUBBLE_Y_MAX: float = 0.78 const LOGO_ASPECT_RATIO: float = 2560.0 / 760.0 const LOGO_WIDTH_FACTOR: float = 0.4784 const LOGO_MIN_WIDTH: float = 294.0 const LOGO_MAX_WIDTH: float = 662.0 const TITLE_HORIZONTAL_MARGIN: float = 48.0 +const TITLE_DESKTOP_REFERENCE_SIZE: Vector2 = Vector2(1280.0, 720.0) +const TITLE_COMPACT_REFERENCE_SIZE: Vector2 = Vector2(640.0, 480.0) const BUBBLE_FIELD_MAX_WIDTH: float = 396.0 const BUBBLE_FIELD_DESKTOP_HEIGHT: float = 318.0 const BUBBLE_FIELD_COMPACT_WIDTH: float = 294.0 @@ -89,10 +101,13 @@ enum ConfirmationAction { @onready var _new_game_label: Label = %NewGameLabel @onready var _delete_save_label: Label = %DeleteSaveLabel @onready var _feedback_label: Label = %FeedbackLabel -@onready var _confirmation_panel: PanelContainer = %ConfirmationPanel -@onready var _confirmation_text: Label = %ConfirmationText -@onready var _confirm_button: Button = %ConfirmButton +@onready var _confirmation_page: TitleConfirmationBubblePageType = ( + %ConfirmationPage +) @onready var _settings_panel: SettingsPanelType = %SettingsPanel +@onready var _title_presentation_scale_root: Control = ( + %TitlePresentationScaleRoot +) @onready var _background: ColorRect = %Background @onready var _decorative_fish_layer: Control = %DecorativeFishLayer @onready var _decorative_fish_timer: Timer = %DecorativeFishTimer @@ -102,13 +117,14 @@ enum ConfirmationAction { @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 _presentation_center: CenterContainer = %Center +@onready var _main_content: VBoxContainer = %MainContent +@onready var _branding_slot: Control = %BrandingSlot @onready var _branding_motion_root: Control = %BrandingMotionRoot -@onready var _title_spacer: Control = $Center/MainContent/Spacer +@onready var _version_row: Control = %VersionRow +@onready var _title_spacer: Control = %Spacer @onready var _button_center: CenterContainer = %ButtonCenter -@onready var _bubble_layout_slot: Control = $Center/MainContent/ButtonCenter/BubbleLayoutSlot +@onready var _bubble_layout_slot: Control = %BubbleLayoutSlot @onready var _bubble_motion_root: Control = %BubbleMotionRoot @onready var _bubble_field: BubbleClusterType = %BubbleField @onready var _start_prompt_center: CenterContainer = %StartPromptCenter @@ -127,6 +143,7 @@ var _decorative_presentation_active: bool = false var _decorative_generation: int = 0 var _decorative_bubbles: Array[TextureRect] = [] var _decorative_bubble_tweens: Dictionary[int, Tween] = {} +var _transition_bubble_ids: Dictionary[int, bool] = {} var _pending_cluster_bubbles: int = 0 var _awaiting_start_input: bool = false var _start_prompt_elapsed: float = 0.0 @@ -150,6 +167,10 @@ var _feedback_text_before_settings: String = "" var _continue_stats_hovered: bool = false var _continue_stats_focused: bool = false var _continue_stats_fade: Tween +var _confirmation_transition: Tween +var _confirmation_transition_generation: int = 0 +var _confirmation_transition_active: bool = false +var _confirmation_title_content_rest_position: Vector2 = Vector2.ZERO func _ready() -> void: @@ -170,11 +191,14 @@ func _ready() -> void: _settings_button.pressed.connect(_open_settings) _delete_button.pressed.connect(_on_delete_pressed) %QuitButton.pressed.connect(_on_quit_pressed) - _confirm_button.pressed.connect(_on_confirmation_accepted) - %CancelConfirmButton.pressed.connect(_close_confirmation) + _confirmation_page.confirmed.connect(_on_confirmation_accepted) + _confirmation_page.cancelled.connect(_request_close_confirmation) _settings_panel.applied.connect(_on_settings_applied) _settings_panel.closed.connect(_on_settings_closed) _settings_panel.opened.connect(_on_settings_opened) + _settings_panel.navigation_transition_started.connect( + _emit_navigation_bubble_flurry + ) _bubble_field.configure(_get_title_buttons()) _decorative_fish_timer.timeout.connect(_on_decorative_fish_timer_timeout) _decorative_bubble_event_timer.timeout.connect( @@ -184,11 +208,11 @@ func _ready() -> void: _on_decorative_bubble_cluster_timer_timeout ) visibility_changed.connect(_on_title_visibility_changed) - resized.connect(_update_title_layout) + resized.connect(_update_responsive_title_stage) _start_prompt_label.resized.connect(_update_start_prompt_pivot) _decorative_rng.randomize() set_process(false) - call_deferred("_update_title_layout") + call_deferred("_update_responsive_title_stage") call_deferred("_update_start_prompt_pivot") call_deferred("_capture_title_bubble_rest_position") call_deferred("_capture_title_content_rest_position") @@ -213,7 +237,7 @@ func reopen() -> void: _cancel_title_settings_transition() Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _prepare_awaiting_start_input() - _close_confirmation() + _reset_confirmation() _settings_panel.hide() _refresh_save_inspection() show() @@ -250,10 +274,14 @@ func is_decorative_bubble_scheduler_active() -> bool: func _update_title_layout() -> void: if not is_node_ready(): return - var available_width: float = maxf(1.0, size.x - TITLE_HORIZONTAL_MARGIN) + var layout_size: Vector2 = _title_presentation_scale_root.size + var available_width: float = maxf( + 1.0, + layout_size.x - TITLE_HORIZONTAL_MARGIN + ) var logo_width: float = minf( clampf( - size.x * LOGO_WIDTH_FACTOR, + layout_size.x * LOGO_WIDTH_FACTOR, LOGO_MIN_WIDTH, LOGO_MAX_WIDTH ), @@ -270,17 +298,17 @@ func _update_title_layout() -> void: logo_width, logo_width / LOGO_ASPECT_RATIO + branding_separation - + _playtest_label.get_combined_minimum_size().y + + _version_row.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 - if size.y < BUBBLE_COMPACT_HEIGHT_THRESHOLD + if layout_size.y < BUBBLE_COMPACT_HEIGHT_THRESHOLD else BUBBLE_FIELD_DESKTOP_HEIGHT ) - if size.y < BUBBLE_COMPACT_HEIGHT_THRESHOLD: + if layout_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) @@ -301,6 +329,35 @@ func _update_title_layout() -> void: _update_world_preview_resolution() +func _update_responsive_title_stage() -> void: + if not is_node_ready(): + return + var display_size := Vector2( + maxf(1.0, size.x), + maxf(1.0, size.y) + ) + var reference_size: Vector2 = ( + TITLE_COMPACT_REFERENCE_SIZE + if display_size.y < BUBBLE_COMPACT_HEIGHT_THRESHOLD + else TITLE_DESKTOP_REFERENCE_SIZE + ) + var presentation_scale: float = minf( + display_size.x / reference_size.x, + display_size.y / reference_size.y + ) + _title_presentation_scale_root.size = reference_size + _title_presentation_scale_root.scale = Vector2.ONE * presentation_scale + _title_presentation_scale_root.position = ( + display_size - reference_size * presentation_scale + ) * 0.5 + _update_title_layout() + if ( + _is_confirmation_active() + and not _confirmation_page.is_transitioning() + ): + _set_confirmation_stage_rect() + + func _get_title_buttons() -> Array[BubbleButton]: return [ _continue_button, @@ -385,7 +442,11 @@ func _process(delta: float) -> void: func _input(event: InputEvent) -> void: if not visible: return - if _title_settings_transition_active or _title_entry_transition_active: + if ( + _title_settings_transition_active + or _title_entry_transition_active + or _confirmation_transition_active + ): get_viewport().set_input_as_handled() return if _awaiting_start_input: @@ -402,8 +463,8 @@ func _input(event: InputEvent) -> void: return if not event.is_action_pressed("ui_cancel"): return - if _confirmation_panel.visible: - _close_confirmation() + if _is_confirmation_active(): + _request_close_confirmation() elif _settings_panel.visible: _settings_panel.handle_back() else: @@ -425,7 +486,7 @@ func _is_start_prompt_reveal_event(event: InputEvent) -> bool: func _handle_primary_menu_focus_input(event: InputEvent) -> bool: if ( not _button_center.visible - or _confirmation_panel.visible + or _is_confirmation_active() or _settings_panel.visible ): return false @@ -616,26 +677,37 @@ func _prepare_intro_presentation(generation: int) -> void: _title_spacer.get_combined_minimum_size().y + float(_main_content.get_theme_constant("separation")) ) - var branding_target_global := Vector2( + var inverse_stage_transform: Transform2D = ( + _title_presentation_scale_root.get_global_transform().affine_inverse() + ) + var branding_anchor_position: Vector2 = ( + inverse_stage_transform + * _intro_branding_anchor.get_global_transform().origin + ) + var branding_slot_position: Vector2 = ( + inverse_stage_transform + * _branding_slot.get_global_transform().origin + ) + var branding_target_position := Vector2( floorf( - _intro_branding_anchor.global_position.x + branding_anchor_position.x - _branding_motion_root.size.x * 0.5 ), floorf( - _intro_branding_anchor.global_position.y + branding_anchor_position.y - (_branding_motion_root.size.y + intro_tail_height) * 0.5 ) ) _branding_intro_position = ( - branding_target_global - - _branding_slot.global_position + branding_target_position + - branding_slot_position ) - var bubble_bounds: Rect2 = _get_title_bubble_global_bounds() + var bubble_bounds: Rect2 = _get_title_bubble_stage_bounds() bubble_bounds.position -= _bubble_motion_root.position var presentation_allowance: float = _get_bubble_offscreen_allowance() _bubble_intro_position = Vector2( 0.0, - size.y + _title_presentation_scale_root.size.y + presentation_allowance - bubble_bounds.position.y ) @@ -646,14 +718,26 @@ func _prepare_intro_presentation(generation: int) -> void: _intro_geometry_ready = true -func _get_title_bubble_global_bounds() -> Rect2: +func _get_title_bubble_stage_bounds() -> Rect2: var bubbles: Array[BubbleButton] = _get_title_buttons() - var bounds: Rect2 = bubbles[0].get_global_rect() + var bounds: Rect2 = _get_title_stage_rect(bubbles[0]) for index: int in range(1, bubbles.size()): - bounds = bounds.merge(bubbles[index].get_global_rect()) + bounds = bounds.merge(_get_title_stage_rect(bubbles[index])) return bounds +func _get_title_stage_rect(control: Control) -> Rect2: + var inverse_stage_transform: Transform2D = ( + _title_presentation_scale_root.get_global_transform().affine_inverse() + ) + var global_rect: Rect2 = control.get_global_rect() + var local_position: Vector2 = ( + inverse_stage_transform * global_rect.position + ) + var local_end: Vector2 = inverse_stage_transform * global_rect.end + return Rect2(local_position, local_end - local_position) + + func _get_bubble_offscreen_allowance() -> float: var maximum_vertical_amplitude: float = 0.0 var maximum_deformation_growth: float = 0.0 @@ -707,7 +791,7 @@ func _update_continue_stats_visibility() -> void: and not _title_entry_transition_active and not _title_settings_transition_active and not _settings_panel.visible - and not _confirmation_panel.visible + and not _is_confirmation_active() and not _action_in_progress and _presentation_center.visible ) @@ -763,7 +847,7 @@ func _get_continue_stats_text() -> String: func _on_continue_pressed() -> void: if ( _action_in_progress - or _confirmation_panel.visible + or _is_confirmation_active() or _settings_panel.visible or _inspection == null or not _inspection.can_continue() @@ -783,7 +867,7 @@ func _on_continue_pressed() -> void: func _on_new_game_pressed() -> void: if ( _action_in_progress - or _confirmation_panel.visible + or _is_confirmation_active() or _settings_panel.visible or _inspection == null ): @@ -805,14 +889,15 @@ func _on_new_game_pressed() -> void: _open_confirmation( ConfirmationAction.NEW_GAME, "start a new game? existing progression will be deleted.", - "start new game" + "start\nnew game", + true ) func _on_delete_pressed() -> void: if ( _action_in_progress - or _confirmation_panel.visible + or _is_confirmation_active() or _settings_panel.visible or _inspection == null or not _inspection.can_delete() @@ -827,22 +912,32 @@ func _on_delete_pressed() -> void: func _on_confirmation_accepted() -> void: - if _action_in_progress: + if ( + _action_in_progress + or _confirmation_transition_active + or _confirmation_action == ConfirmationAction.NONE + ): return var action: ConfirmationAction = _confirmation_action - _close_confirmation() + _confirmation_page.lock_interaction() _action_in_progress = true if not _save_manager.delete_progression_save(): _feedback_label.text = "failed to delete saved progression." _action_in_progress = false _refresh_save_inspection() + _begin_confirmation_return() return _save_manager.initialize_new_game() _refresh_save_inspection() if action == ConfirmationAction.NEW_GAME: + _confirmation_action = ConfirmationAction.NONE + _confirmation_transition_generation += 1 + _cancel_confirmation_transition() + _confirmation_page.hide_page() gameplay_requested.emit() else: _feedback_label.text = "saved progression deleted." + _begin_confirmation_return() _action_in_progress = false @@ -850,33 +945,177 @@ func _open_confirmation( action: ConfirmationAction, message: String, confirm_text: String, + use_multiline_action_layout: bool = false, ) -> void: _hide_continue_stats_context() _modal_restore_navigation_focus = _navigation_focus_active _confirmation_action = action - _confirmation_text.text = message - _confirm_button.text = confirm_text - _confirmation_panel.visible = true - _confirm_button.grab_focus() + _confirmation_page.configure( + message, + confirm_text, + use_multiline_action_layout + ) + _begin_confirmation_open() -func _close_confirmation() -> void: +func _request_close_confirmation() -> void: + if ( + not _confirmation_page.visible + or _confirmation_transition_active + or _action_in_progress + ): + return + _begin_confirmation_return() + + +func _begin_confirmation_open() -> void: + _confirmation_transition_generation += 1 + _cancel_confirmation_transition() + _confirmation_transition_active = true + _emit_navigation_bubble_flurry() + _confirmation_title_content_rest_position = _presentation_center.position + _set_confirmation_stage_rect() + _confirmation_page.hide_page() + _set_title_bubbles_interactive(false) + _release_primary_menu_focus() + var generation: int = _confirmation_transition_generation + var content_bounds: Rect2 = _get_title_stage_rect(_main_content) + var exit_distance: float = ( + content_bounds.end.y + HOST_CLUSTER_SAFE_MARGIN + ) + _confirmation_transition = create_tween() + _confirmation_transition.tween_property( + _presentation_center, + "position:y", + _confirmation_title_content_rest_position.y - exit_distance, + HOST_PRESENTATION_OUT_DURATION + ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + _confirmation_transition.finished.connect( + _finish_confirmation_title_exit.bind(generation), + CONNECT_ONE_SHOT + ) + + +func _finish_confirmation_title_exit(generation: int) -> void: + if ( + generation != _confirmation_transition_generation + or not _confirmation_transition_active + ): + return + _confirmation_transition = null + _presentation_center.hide() + _presentation_center.position = _confirmation_title_content_rest_position + _confirmation_page.transition_in( + HOST_PRESENTATION_IN_DURATION, + _finish_confirmation_open.bind(generation) + ) + + +func _finish_confirmation_open(generation: int) -> void: + if ( + generation != _confirmation_transition_generation + or not _confirmation_transition_active + ): + return + _confirmation_transition_active = false + + +func _begin_confirmation_return() -> void: + if not _confirmation_page.visible or _confirmation_transition_active: + return + _confirmation_transition_generation += 1 + _cancel_confirmation_transition() + _confirmation_transition_active = true + _emit_navigation_bubble_flurry() + _confirmation_page.lock_interaction() + var generation: int = _confirmation_transition_generation + _confirmation_page.transition_out( + HOST_PRESENTATION_OUT_DURATION, + _finish_confirmation_page_exit.bind(generation) + ) + + +func _finish_confirmation_page_exit(generation: int) -> void: + if ( + generation != _confirmation_transition_generation + or not _confirmation_transition_active + ): + return + _confirmation_action = ConfirmationAction.NONE + _presentation_center.position = Vector2( + _confirmation_title_content_rest_position.x, + _title_presentation_scale_root.size.y + + HOST_CLUSTER_SAFE_MARGIN + ) + _presentation_center.show() + _confirmation_transition = create_tween() + _confirmation_transition.tween_property( + _presentation_center, + "position", + _confirmation_title_content_rest_position, + HOST_PRESENTATION_IN_DURATION + ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + _confirmation_transition.finished.connect( + _finish_confirmation_return.bind(generation), + CONNECT_ONE_SHOT + ) + + +func _finish_confirmation_return(generation: int) -> void: + if ( + generation != _confirmation_transition_generation + or not _confirmation_transition_active + ): + return + _confirmation_transition = null + _confirmation_transition_active = false + _presentation_center.position = _confirmation_title_content_rest_position + _set_title_bubbles_interactive(true) var restore_navigation_focus: bool = _modal_restore_navigation_focus _modal_restore_navigation_focus = false - _confirmation_action = ConfirmationAction.NONE - _confirmation_panel.visible = false - if _awaiting_start_input: - return if restore_navigation_focus: _focus_initial_button() else: _navigation_focus_active = false _release_title_focus() + _update_continue_stats_visibility() + + +func _set_confirmation_stage_rect() -> void: + var slot_rect: Rect2 = _get_title_stage_rect(_bubble_layout_slot) + _confirmation_page.set_stage_rect( + slot_rect + ) + + +func _reset_confirmation() -> void: + _confirmation_transition_generation += 1 + _cancel_confirmation_transition() + _confirmation_transition_active = false + _confirmation_action = ConfirmationAction.NONE + _confirmation_page.hide_page() + if is_node_ready(): + _presentation_center.position = _confirmation_title_content_rest_position + _presentation_center.show() + + +func _cancel_confirmation_transition() -> void: + if _confirmation_transition != null: + _confirmation_transition.kill() + _confirmation_transition = null + + +func _is_confirmation_active() -> bool: + return ( + _confirmation_action != ConfirmationAction.NONE + or _confirmation_page.visible + or _confirmation_transition_active + ) func _open_settings() -> void: if ( - _confirmation_panel.visible + _is_confirmation_active() or _action_in_progress or _settings_panel.visible or _title_settings_transition_active @@ -909,15 +1148,15 @@ func _begin_title_cluster_exit() -> void: _title_settings_transition_generation += 1 _cancel_title_settings_tween() _title_settings_transition_active = true + _emit_navigation_bubble_flurry() _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 content_bounds: Rect2 = _get_title_stage_rect(_main_content) var exit_distance: float = ( - _main_content.global_position.y - + _main_content.size.y - + HOST_CLUSTER_SAFE_MARGIN + content_bounds.end.y + HOST_CLUSTER_SAFE_MARGIN ) _title_settings_transition = create_tween() _title_settings_transition.tween_property( @@ -957,17 +1196,9 @@ func _begin_title_cluster_return(feedback_text: String) -> void: _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 + _title_presentation_scale_root.size.y + HOST_CLUSTER_SAFE_MARGIN ) _presentation_center.show() _title_settings_transition = create_tween() @@ -997,7 +1228,6 @@ func _finish_title_cluster_return(generation: int) -> void: _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 = ( @@ -1094,7 +1324,7 @@ func _focus_initial_button() -> void: func _on_quit_pressed() -> void: if ( not _action_in_progress - and not _confirmation_panel.visible + and not _is_confirmation_active() and not _settings_panel.visible ): _hide_continue_stats_context() @@ -1112,6 +1342,7 @@ func _on_title_visibility_changed() -> void: _stop_entry_prompt_animation() _navigation_focus_active = false _modal_restore_navigation_focus = false + _reset_confirmation() _stop_decorative_presentation() @@ -1155,6 +1386,7 @@ func _stop_decorative_presentation() -> void: if is_instance_valid(bubble): bubble.queue_free() _decorative_bubbles.clear() + _transition_bubble_ids.clear() func _schedule_next_decorative_fish( @@ -1338,7 +1570,7 @@ func _on_decorative_bubble_event_timer_timeout() -> void: if not _decorative_presentation_active or not visible: return var available_slots: int = ( - MAX_DECORATIVE_BUBBLES - _decorative_bubbles.size() + MAX_DECORATIVE_BUBBLES - _get_idle_decorative_bubble_count() ) if available_slots <= 0: _schedule_next_decorative_bubble_event( @@ -1387,7 +1619,7 @@ func _on_decorative_bubble_cluster_timer_timeout() -> void: ): _pending_cluster_bubbles = 0 return - if _decorative_bubbles.size() < MAX_DECORATIVE_BUBBLES: + if _get_idle_decorative_bubble_count() < MAX_DECORATIVE_BUBBLES: _spawn_decorative_bubble(_decorative_generation) _pending_cluster_bubbles -= 1 if _pending_cluster_bubbles > 0: @@ -1399,15 +1631,24 @@ func _on_decorative_bubble_cluster_timer_timeout() -> void: ) -func _spawn_decorative_bubble(generation: int) -> bool: +func _spawn_decorative_bubble( + generation: int, + start_y_ratio: float = -1.0, + normalized_x_override: float = -1.0, + is_transition_bubble: bool = false, +) -> bool: if ( generation != _decorative_generation or not _decorative_presentation_active or not visible - or _decorative_bubbles.size() >= MAX_DECORATIVE_BUBBLES or DECORATIVE_BUBBLE_TEXTURES.is_empty() ): return false + if is_transition_bubble: + if _transition_bubble_ids.size() >= MAX_TRANSITION_BUBBLES: + return false + elif _get_idle_decorative_bubble_count() >= MAX_DECORATIVE_BUBBLES: + return false var layer_size: Vector2 = _decorative_bubble_layer.size if layer_size.x <= 1.0 or layer_size.y <= 1.0: return false @@ -1436,8 +1677,12 @@ func _spawn_decorative_bubble(generation: int) -> bool: ) _decorative_bubble_layer.add_child(bubble) _decorative_bubbles.append(bubble) + if is_transition_bubble: + _transition_bubble_ids[bubble.get_instance_id()] = true - var normalized_x: float = _decorative_rng.randf_range(0.08, 0.92) + var normalized_x: float = normalized_x_override + if normalized_x < 0.0: + normalized_x = _decorative_rng.randf_range(0.08, 0.92) var drift_direction: float = ( 1.0 if _decorative_rng.randi_range(0, 1) == 1 else -1.0 ) @@ -1458,6 +1703,28 @@ func _spawn_decorative_bubble(generation: int) -> bool: BUBBLE_TRAVEL_DURATION_MIN, BUBBLE_TRAVEL_DURATION_MAX ) + if start_y_ratio >= 0.0: + var full_start_y: float = ( + layer_size.y + presentation_size.y + BUBBLE_EDGE_MARGIN + ) + var end_y: float = -presentation_size.y - BUBBLE_EDGE_MARGIN + var burst_start_y: float = ( + start_y_ratio * layer_size.y - presentation_size.y * 0.5 + ) + var remaining_distance: float = maxf(burst_start_y - end_y, 1.0) + var full_distance: float = maxf(full_start_y - end_y, 1.0) + travel_duration *= remaining_distance / full_distance + _update_decorative_bubble( + 0.0, + bubble, + normalized_x, + horizontal_drift, + wobble_amplitude, + wobble_cycles, + wobble_phase, + start_y_ratio, + generation + ) var bubble_tween: Tween = create_tween() _decorative_bubble_tweens[bubble.get_instance_id()] = bubble_tween bubble_tween.tween_method( @@ -1468,6 +1735,7 @@ func _spawn_decorative_bubble(generation: int) -> bool: wobble_amplitude, wobble_cycles, wobble_phase, + start_y_ratio, generation ), 0.0, @@ -1489,6 +1757,7 @@ func _update_decorative_bubble( wobble_amplitude: float, wobble_cycles: float, wobble_phase: float, + start_y_ratio: float, generation: int, ) -> void: if ( @@ -1501,6 +1770,8 @@ func _update_decorative_bubble( var start_y: float = ( layer_size.y + bubble.size.y + BUBBLE_EDGE_MARGIN ) + if start_y_ratio >= 0.0: + start_y = start_y_ratio * layer_size.y - bubble.size.y * 0.5 var end_y: float = -bubble.size.y - BUBBLE_EDGE_MARGIN var base_x: float = normalized_x * layer_size.x var wobble: float = sin( @@ -1523,9 +1794,37 @@ func _on_decorative_bubble_finished( return if is_instance_valid(bubble): _decorative_bubble_tweens.erase(bubble.get_instance_id()) + _transition_bubble_ids.erase(bubble.get_instance_id()) _decorative_bubbles.erase(bubble) bubble.queue_free() +func _get_idle_decorative_bubble_count() -> int: + return _decorative_bubbles.size() - _transition_bubble_ids.size() + + +func _emit_navigation_bubble_flurry() -> void: + if not _decorative_presentation_active or not visible: + return + var bubble_count: int = _decorative_rng.randi_range( + TRANSITION_BUBBLE_COUNT_MIN, + TRANSITION_BUBBLE_COUNT_MAX + ) + for _bubble_index: int in bubble_count: + if not _spawn_decorative_bubble( + _decorative_generation, + _decorative_rng.randf_range( + TRANSITION_BUBBLE_Y_MIN, + TRANSITION_BUBBLE_Y_MAX + ), + _decorative_rng.randf_range( + TRANSITION_BUBBLE_X_MIN, + TRANSITION_BUBBLE_X_MAX + ), + true + ): + break + + func _exit_tree() -> void: _stop_decorative_presentation() diff --git a/ui/title_screen.tscn b/ui/title_screen.tscn index ca6b8df..4f7e080 100644 --- a/ui/title_screen.tscn +++ b/ui/title_screen.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=12 format=3] +[gd_scene load_steps=13 format=3] [ext_resource type="Script" path="res://ui/title_screen.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] @@ -9,6 +9,7 @@ [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_button.gd" id="7_bubble_button"] [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_cluster.gd" id="8_bubble_cluster"] [ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="9_bubble_profile"] +[ext_resource type="PackedScene" path="res://ui/title_confirmation_bubble_page.tscn" id="10_confirmation_page"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water"] shader = ExtResource("4_water_shader") @@ -76,7 +77,25 @@ one_shot = true unique_name_in_owner = true one_shot = true -[node name="StartPromptCenter" type="CenterContainer" parent="."] +[node name="ResponsiveTitleStage" type="Control" parent="."] +unique_name_in_owner = true +z_index = 10 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 1 + +[node name="TitlePresentationScaleRoot" type="Control" parent="ResponsiveTitleStage"] +unique_name_in_owner = true +layout_mode = 0 +offset_right = 1280.0 +offset_bottom = 720.0 +mouse_filter = 1 + +[node name="StartPromptCenter" type="CenterContainer" parent="ResponsiveTitleStage/TitlePresentationScaleRoot"] unique_name_in_owner = true z_index = 12 layout_mode = 1 @@ -89,7 +108,7 @@ grow_horizontal = 2 grow_vertical = 0 mouse_filter = 2 -[node name="StartPromptLabel" type="Label" parent="StartPromptCenter"] +[node name="StartPromptLabel" type="Label" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/StartPromptCenter"] unique_name_in_owner = true layout_mode = 2 mouse_filter = 2 @@ -101,7 +120,7 @@ text = "press any key to start" horizontal_alignment = 1 vertical_alignment = 1 -[node name="IntroBrandingAnchor" type="Control" parent="."] +[node name="IntroBrandingAnchor" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot"] unique_name_in_owner = true layout_mode = 1 anchor_left = 0.5 @@ -112,7 +131,8 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 -[node name="Center" type="CenterContainer" parent="."] +[node name="Center" type="CenterContainer" parent="ResponsiveTitleStage/TitlePresentationScaleRoot"] +unique_name_in_owner = true z_index = 10 layout_mode = 1 anchors_preset = 15 @@ -121,23 +141,25 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="MainContent" type="VBoxContainer" parent="Center"] +[node name="MainContent" type="VBoxContainer" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center"] +unique_name_in_owner = true layout_mode = 2 theme_override_constants/separation = 7 alignment = 1 -[node name="BrandingSlot" type="Control" parent="Center/MainContent"] +[node name="BrandingSlot" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"] +unique_name_in_owner = true custom_minimum_size = Vector2(640, 215) layout_mode = 2 -[node name="BrandingMotionRoot" type="Control" parent="Center/MainContent/BrandingSlot"] +[node name="BrandingMotionRoot" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/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"] +[node name="BrandingContent" type="VBoxContainer" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/BrandingSlot/BrandingMotionRoot"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -146,7 +168,7 @@ grow_horizontal = 2 grow_vertical = 2 theme_override_constants/separation = 7 -[node name="TitleLogo" type="TextureRect" parent="Center/MainContent/BrandingSlot/BrandingMotionRoot/BrandingContent"] +[node name="TitleLogo" type="TextureRect" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/BrandingSlot/BrandingMotionRoot/BrandingContent"] unique_name_in_owner = true custom_minimum_size = Vector2(640, 190) material = SubResource("ShaderMaterial_title_logo") @@ -157,35 +179,57 @@ texture = ExtResource("5_logo") expand_mode = 1 stretch_mode = 5 -[node name="PlaytestLabel" type="Label" parent="Center/MainContent/BrandingSlot/BrandingMotionRoot/BrandingContent"] +[node name="VersionRow" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/BrandingSlot/BrandingMotionRoot/BrandingContent"] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 18) +layout_mode = 2 +mouse_filter = 2 + +[node name="VersionAnchor" type="CenterContainer" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/BrandingSlot/BrandingMotionRoot/BrandingContent/VersionRow"] +layout_mode = 1 +anchor_left = 0.7 +anchor_top = 0.5 +anchor_right = 0.7 +anchor_bottom = 0.5 +offset_left = -140.0 +offset_top = -17.0 +offset_right = 140.0 +offset_bottom = 11.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="PlaytestLabel" type="Label" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/BrandingSlot/BrandingMotionRoot/BrandingContent/VersionRow/VersionAnchor"] 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 +theme_override_font_sizes/font_size = 22 text = "pre-alpha playtest 0.2" horizontal_alignment = 1 -[node name="Spacer" type="Control" parent="Center/MainContent"] +[node name="Spacer" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"] +unique_name_in_owner = true custom_minimum_size = Vector2(0, 3) layout_mode = 2 -[node name="ButtonCenter" type="CenterContainer" parent="Center/MainContent"] +[node name="ButtonCenter" type="CenterContainer" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"] unique_name_in_owner = true visible = false layout_mode = 2 -[node name="BubbleLayoutSlot" type="Control" parent="Center/MainContent/ButtonCenter"] +[node name="BubbleLayoutSlot" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter"] +unique_name_in_owner = true custom_minimum_size = Vector2(396, 318) layout_mode = 2 -[node name="BubbleMotionRoot" type="Control" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot"] +[node name="BubbleMotionRoot" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/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"] +[node name="BubbleField" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot"] unique_name_in_owner = true custom_minimum_size = Vector2(396, 318) layout_mode = 0 @@ -194,7 +238,7 @@ offset_bottom = 318.0 script = ExtResource("8_bubble_cluster") profile = ExtResource("9_bubble_profile") -[node name="ContinueButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] +[node name="ContinueButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true offset_left = 123.0 offset_top = 86.0 @@ -214,7 +258,7 @@ motion_period = 5.8 deformation_amplitude = 0.01 deformation_period = 6.7 -[node name="NewGameButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] +[node name="NewGameButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true offset_left = 42.0 offset_top = 4.0 @@ -236,7 +280,7 @@ motion_phase = 1.2 deformation_amplitude = 0.018 deformation_period = 5.9 -[node name="NewGameLabel" type="Label" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField/NewGameButton"] +[node name="NewGameLabel" type="Label" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField/NewGameButton"] unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 @@ -251,7 +295,7 @@ text = "new game" horizontal_alignment = 1 vertical_alignment = 1 -[node name="SettingsButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] +[node name="SettingsButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true offset_left = 264.0 offset_top = 17.0 @@ -270,7 +314,7 @@ motion_period = 4.9 motion_phase = 2.5 deformation_period = 6.3 -[node name="DeleteSaveButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] +[node name="DeleteSaveButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true offset_left = -11.0 offset_top = 122.0 @@ -291,7 +335,7 @@ motion_phase = 3.7 deformation_amplitude = 0.018 deformation_period = 5.4 -[node name="DeleteSaveLabel" type="Label" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField/DeleteSaveButton"] +[node name="DeleteSaveLabel" type="Label" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField/DeleteSaveButton"] unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 @@ -306,7 +350,7 @@ text = "delete save" horizontal_alignment = 1 vertical_alignment = 1 -[node name="QuitButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] +[node name="QuitButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true offset_left = 275.0 offset_top = 217.0 @@ -328,7 +372,7 @@ motion_phase = 4.8 deformation_amplitude = 0.02 deformation_period = 4.8 -[node name="FeedbackLabel" type="Label" parent="Center/MainContent"] +[node name="FeedbackLabel" type="Label" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"] unique_name_in_owner = true visible = false custom_minimum_size = Vector2(0, 44) @@ -338,59 +382,17 @@ horizontal_alignment = 1 vertical_alignment = 1 autowrap_mode = 2 -[node name="ConfirmationPanel" type="PanelContainer" parent="."] +[node name="ConfirmationPage" parent="ResponsiveTitleStage/TitlePresentationScaleRoot" instance=ExtResource("10_confirmation_page")] unique_name_in_owner = true -visible = false -z_index = 210 +z_index = 11 layout_mode = 1 -anchors_preset = 8 -anchor_left = 0.5 -anchor_top = 0.5 -anchor_right = 0.5 -anchor_bottom = 0.5 -offset_left = -320.0 -offset_top = -150.0 -offset_right = 320.0 -offset_bottom = 150.0 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="Margin" type="MarginContainer" parent="ConfirmationPanel"] -layout_mode = 2 -theme_override_constants/margin_left = 24 -theme_override_constants/margin_top = 20 -theme_override_constants/margin_right = 24 -theme_override_constants/margin_bottom = 20 - -[node name="Content" type="VBoxContainer" parent="ConfirmationPanel/Margin"] -layout_mode = 2 -theme_override_constants/separation = 14 - -[node name="ConfirmationText" type="Label" parent="ConfirmationPanel/Margin/Content"] -unique_name_in_owner = true -layout_mode = 2 -text = "confirm?" -horizontal_alignment = 1 -autowrap_mode = 2 - -[node name="Buttons" type="HBoxContainer" parent="ConfirmationPanel/Margin/Content"] -layout_mode = 2 -theme_override_constants/separation = 12 -alignment = 1 - -[node name="ConfirmButton" type="Button" parent="ConfirmationPanel/Margin/Content/Buttons"] -unique_name_in_owner = true -custom_minimum_size = Vector2(190, 64) -layout_mode = 2 -text = "confirm" - -[node name="CancelConfirmButton" type="Button" parent="ConfirmationPanel/Margin/Content/Buttons"] -unique_name_in_owner = true -custom_minimum_size = Vector2(160, 64) -layout_mode = 2 -text = "cancel" - -[node name="SettingsPanel" parent="." instance=ExtResource("3_settings")] +[node name="SettingsPanel" parent="ResponsiveTitleStage/TitlePresentationScaleRoot" instance=ExtResource("3_settings")] unique_name_in_owner = true z_index = 210 layout_mode = 1