diff --git a/ui/components/bubble_menu/README.md b/ui/components/bubble_menu/README.md new file mode 100644 index 0000000..ca549bb --- /dev/null +++ b/ui/components/bubble_menu/README.md @@ -0,0 +1,26 @@ +# Bubble menu authoring + +Instance `bubble_button.tscn` for each action, or attach `bubble_button.gd` +to an existing Button. Set its neutral size, desktop and compact anchors, +font-size limits, and deterministic motion values in the Inspector. A label +child can be assigned through `label_control_path` when wrapped text is needed. +Text size is calculated once per layout from the neutral smaller dimension and +the profile ratio. + +Place the buttons under a Control using `bubble_cluster.gd`. Give the cluster +the ordered BubbleButton references with `configure()`, then call +`apply_layout()` when its available size or responsive layout changes. The +button order defines explicit keyboard and controller focus neighbors. Compact +anchors and minimum sizes remain authored per button. + +The shared profile owns the palette, rounded styles, proportional-font ratio, +hover response, and contact tuning. Labels, actions, sizes, anchors, per-button +motion, responsive wrapping, availability, and confirmation behavior remain +owned by the menu. Connect each Button's `pressed` signal in that parent menu. + +`motion_scale` defaults to `1.0`. Setting it to `0.0` removes idle drift and +deformation while retaining hover and focus feedback. + +Contact is a deterministic, bounded visual correction around authored anchors. +Real physics is intentionally avoided so layouts, focus order, and hit targets +remain stable. diff --git a/ui/components/bubble_menu/bubble_button.gd b/ui/components/bubble_menu/bubble_button.gd new file mode 100644 index 0000000..15acb0e --- /dev/null +++ b/ui/components/bubble_menu/bubble_button.gd @@ -0,0 +1,186 @@ +class_name BubbleButton +extends Button + +@export var profile: BubbleMenuProfile + +@export_group("Authored Layout") +@export var neutral_size: Vector2 = Vector2(120.0, 116.0) +@export var desktop_anchor: Vector2 = Vector2.ZERO +@export var compact_anchor: Vector2 = Vector2.ZERO +@export var compact_minimum_size: Vector2 = Vector2.ZERO + +@export_group("Typography") +@export var label_control_path: NodePath +@export_range(1, 256, 1) var minimum_font_size: int = 14 +@export_range(1, 256, 1) var maximum_font_size: int = 32 + +@export_group("Motion") +@export_range(0.0, 32.0, 0.1) var horizontal_amplitude: float = 1.8 +@export_range(0.0, 32.0, 0.1) var vertical_amplitude: float = 4.0 +@export_range(0.1, 30.0, 0.1) var motion_period: float = 5.0 +@export_range(0.0, TAU, 0.01) var motion_phase: float = 0.0 +@export_range(0.0, 0.2, 0.001) var deformation_amplitude: float = 0.016 +@export_range(0.1, 30.0, 0.1) var deformation_period: float = 6.0 + +var neutral_position: Vector2 = Vector2.ZERO +var presented_size: Vector2 = Vector2.ZERO +var emphasis: float = 0.0 +var _hovered: bool = false +var _focused: bool = false + + +func _ready() -> void: + mouse_entered.connect(_set_hovered.bind(true)) + mouse_exited.connect(_set_hovered.bind(false)) + focus_entered.connect(_set_focused.bind(true)) + focus_exited.connect(_set_focused.bind(false)) + resized.connect(_update_pivot) + _update_pivot() + apply_profile() + + +func apply_layout( + center: Vector2, + bubble_size: Vector2, + font_size_ratio: float, +) -> void: + position = center - bubble_size * 0.5 + size = bubble_size + neutral_position = position + presented_size = bubble_size + _update_pivot() + var label_control: Control = get_label_control() + var font_size := clampi( + roundi(minf(bubble_size.x, bubble_size.y) * font_size_ratio), + minimum_font_size, + maximum_font_size + ) + label_control.add_theme_font_size_override("font_size", font_size) + + +func get_layout_size(layout_scale: float, compact: bool) -> Vector2: + var bubble_size: Vector2 = neutral_size * layout_scale + if compact: + bubble_size.x = maxf(bubble_size.x, compact_minimum_size.x) + bubble_size.y = maxf(bubble_size.y, compact_minimum_size.y) + return bubble_size + + +func get_authored_anchor(compact: bool) -> Vector2: + return compact_anchor if compact else desktop_anchor + + +func get_label_control() -> Control: + if not label_control_path.is_empty(): + var custom_label := get_node_or_null(label_control_path) as Control + if custom_label != null: + return custom_label + return self + + +func advance_emphasis(delta: float) -> void: + var target: float = 1.0 if _hovered or _focused else 0.0 + emphasis = move_toward( + emphasis, + target, + profile.emphasis_speed * delta + ) + + +func calculate_target( + elapsed: float, + layout_scale: float, + motion_scale: float, +) -> Vector2: + var phase: float = ( + elapsed / motion_period * TAU + + motion_phase + ) + var idle_offset := Vector2( + sin(phase * 0.73 + motion_phase) * horizontal_amplitude, + sin(phase) * vertical_amplitude + ) * layout_scale * motion_scale + idle_offset.y -= ( + profile.hover_focus_lift + * emphasis + * layout_scale + ) + return neutral_position + idle_offset + + +func calculate_visual_scale( + elapsed: float, + motion_scale: float, +) -> Vector2: + var deformation_phase: float = ( + elapsed / deformation_period * TAU + + motion_phase * 1.37 + ) + var deformation_amount: float = ( + sin(deformation_phase) + * deformation_amplitude + * motion_scale + * lerpf(1.0, profile.emphasized_deformation_scale, emphasis) + ) + var hover_scale: float = lerpf( + 1.0, + profile.hover_focus_scale, + emphasis + ) + return Vector2( + 1.0 + deformation_amount, + 1.0 - deformation_amount + ) * hover_scale + + +func get_visual_radius(visual_scale: Vector2) -> float: + var visual_size: Vector2 = presented_size * visual_scale + return (visual_size.x + visual_size.y) * 0.25 + + +func apply_presentation( + target_position: Vector2, + visual_scale: Vector2, + position_weight: float, +) -> void: + position = position.lerp(target_position, position_weight) + scale = visual_scale + + +func _has_point(point: Vector2) -> bool: + var radius: Vector2 = size * 0.5 + if radius.x <= 0.0 or radius.y <= 0.0: + return false + var normalized: Vector2 = (point - radius) / radius + return normalized.length_squared() <= 1.0 + + +func _set_hovered(value: bool) -> void: + _hovered = value + + +func _set_focused(value: bool) -> void: + _focused = value + + +func _update_pivot() -> void: + pivot_offset = size * 0.5 + + +func apply_profile() -> void: + if profile == null: + return + add_theme_stylebox_override("normal", profile.make_normal_style()) + var hover_style: StyleBoxFlat = profile.make_hover_style() + add_theme_stylebox_override("hover", hover_style) + add_theme_stylebox_override("focus", hover_style) + add_theme_stylebox_override("pressed", profile.make_pressed_style()) + add_theme_stylebox_override("disabled", profile.make_disabled_style()) + add_theme_color_override("font_color", profile.text_color) + add_theme_color_override("font_hover_color", profile.text_hover_color) + add_theme_color_override("font_focus_color", profile.text_hover_color) + add_theme_color_override("font_pressed_color", profile.text_pressed_color) + add_theme_color_override("font_disabled_color", profile.text_disabled_color) + var label_control: Control = get_label_control() + if label_control != self: + label_control.add_theme_color_override("font_color", profile.text_color) diff --git a/ui/components/bubble_menu/bubble_button.gd.uid b/ui/components/bubble_menu/bubble_button.gd.uid new file mode 100644 index 0000000..ede0b1d --- /dev/null +++ b/ui/components/bubble_menu/bubble_button.gd.uid @@ -0,0 +1 @@ +uid://d15kke1iwoueg diff --git a/ui/components/bubble_menu/bubble_button.tscn b/ui/components/bubble_menu/bubble_button.tscn new file mode 100644 index 0000000..7fdc596 --- /dev/null +++ b/ui/components/bubble_menu/bubble_button.tscn @@ -0,0 +1,9 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_button.gd" id="1_button"] +[ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="2_profile"] + +[node name="BubbleButton" type="Button"] +custom_minimum_size = Vector2(120, 116) +script = ExtResource("1_button") +profile = ExtResource("2_profile") diff --git a/ui/components/bubble_menu/bubble_cluster.gd b/ui/components/bubble_menu/bubble_cluster.gd new file mode 100644 index 0000000..9a8d4d0 --- /dev/null +++ b/ui/components/bubble_menu/bubble_cluster.gd @@ -0,0 +1,117 @@ +class_name BubbleCluster +extends Control + +@export var profile: BubbleMenuProfile +@export var desktop_reference_size: Vector2 = Vector2(396.0, 318.0) +@export var compact_reference_size: Vector2 = Vector2(294.0, 200.0) +@export_range(0.0, 1.0, 0.01) var motion_scale: float = 1.0 + +var _bubbles: Array[BubbleButton] = [] +var _elapsed: float = 0.0 +var _layout_scale: float = 1.0 + + +func configure(bubbles: Array[BubbleButton]) -> void: + _bubbles = bubbles + for index: int in _bubbles.size(): + var bubble: BubbleButton = _bubbles[index] + if bubble.profile == null: + bubble.profile = profile + bubble.apply_profile() + if index > 0: + bubble.focus_neighbor_top = bubble.get_path_to( + _bubbles[index - 1] + ) + if index + 1 < _bubbles.size(): + bubble.focus_neighbor_bottom = bubble.get_path_to( + _bubbles[index + 1] + ) + + +func apply_layout(field_size: Vector2, compact: bool) -> void: + _layout_scale = minf( + field_size.x / desktop_reference_size.x, + field_size.y / desktop_reference_size.y + ) + var authored_extent: Vector2 = desktop_reference_size * _layout_scale + var layout_origin := Vector2( + (field_size.x - authored_extent.x) * 0.5, + (field_size.y - authored_extent.y) * 0.5 + ) + for bubble: BubbleButton in _bubbles: + var bubble_size: Vector2 = bubble.get_layout_size( + _layout_scale, + compact + ) + var center: Vector2 = ( + layout_origin + + bubble.get_authored_anchor(compact) * _layout_scale + ) + bubble.apply_layout( + center, + bubble_size, + profile.font_size_ratio + ) + + +func advance_motion(delta: float) -> void: + _elapsed = fmod(_elapsed + delta, 120.0) + var targets: Array[Vector2] = [] + var visual_radii: Array[float] = [] + var visual_scales: Array[Vector2] = [] + for bubble: BubbleButton in _bubbles: + bubble.advance_emphasis(delta) + var target: Vector2 = bubble.calculate_target( + _elapsed, + _layout_scale, + motion_scale + ) + var visual_scale: Vector2 = bubble.calculate_visual_scale( + _elapsed, + motion_scale + ) + targets.append(target) + visual_scales.append(visual_scale) + visual_radii.append(bubble.get_visual_radius(visual_scale)) + for first_index: int in _bubbles.size(): + for second_index: int in range(first_index + 1, _bubbles.size()): + var first_center: Vector2 = ( + targets[first_index] + + _bubbles[first_index].presented_size * 0.5 + ) + var second_center: Vector2 = ( + targets[second_index] + + _bubbles[second_index].presented_size * 0.5 + ) + var center_delta: Vector2 = second_center - first_center + var distance: float = center_delta.length() + var desired_distance: float = ( + visual_radii[first_index] + + visual_radii[second_index] + + profile.contact_gap * _layout_scale + ) + if distance >= desired_distance: + continue + var direction := ( + center_delta / distance + if distance > 0.001 + else Vector2.RIGHT.rotated(float(first_index + 1)) + ) + var correction: Vector2 = ( + direction + * minf( + (desired_distance - distance) * 0.5, + profile.maximum_separation * _layout_scale + ) + ) + targets[first_index] -= correction + targets[second_index] += correction + var position_weight: float = 1.0 - exp( + -profile.position_response * delta + ) + for index: int in _bubbles.size(): + _bubbles[index].apply_presentation( + targets[index], + visual_scales[index], + position_weight + ) diff --git a/ui/components/bubble_menu/bubble_cluster.gd.uid b/ui/components/bubble_menu/bubble_cluster.gd.uid new file mode 100644 index 0000000..08becd9 --- /dev/null +++ b/ui/components/bubble_menu/bubble_cluster.gd.uid @@ -0,0 +1 @@ +uid://cvqif55pw6r32 diff --git a/ui/components/bubble_menu/bubble_menu_profile.gd b/ui/components/bubble_menu/bubble_menu_profile.gd new file mode 100644 index 0000000..a78cbd1 --- /dev/null +++ b/ui/components/bubble_menu/bubble_menu_profile.gd @@ -0,0 +1,91 @@ +class_name BubbleMenuProfile +extends Resource + +@export_group("Typography") +@export_range(0.01, 1.0, 0.001) var font_size_ratio: float = 0.17 + +@export_group("Interaction") +@export_range(1.0, 1.2, 0.001) var hover_focus_scale: float = 1.035 +@export_range(0.0, 16.0, 0.1) var hover_focus_lift: float = 2.0 +@export_range(0.1, 30.0, 0.1) var emphasis_speed: float = 10.0 +@export_range(0.0, 1.0, 0.01) var emphasized_deformation_scale: float = 0.6 + +@export_group("Cluster") +@export_range(0.1, 30.0, 0.1) var position_response: float = 8.0 +@export_range(0.0, 24.0, 0.1) var contact_gap: float = 3.0 +@export_range(0.0, 24.0, 0.1) var maximum_separation: float = 3.0 + +@export_group("Shape") +@export_range(0.0, 32.0, 0.5) var content_margin: float = 8.0 +@export_range(0, 512, 1) var corner_radius: int = 128 +@export_range(0, 12, 1) var normal_border_width: int = 2 +@export_range(0, 12, 1) var emphasized_border_width: int = 3 + +@export_group("Colors") +@export var normal_fill: Color = Color(0.82, 0.93, 0.96, 0.96) +@export var normal_border: Color = Color(0.91, 0.98, 1.0, 0.85) +@export var hover_fill: Color = Color(0.91, 0.975, 0.985, 1.0) +@export var hover_border: Color = Color(1.0, 1.0, 1.0, 0.95) +@export var pressed_fill: Color = Color(0.7, 0.87, 0.92, 1.0) +@export var pressed_border: Color = Color(0.94, 0.99, 1.0, 1.0) +@export var disabled_fill: Color = Color(0.66, 0.77, 0.8, 0.78) +@export var disabled_border: Color = Color(0.81, 0.9, 0.92, 0.55) +@export var text_color: Color = Color(0.035, 0.145, 0.22, 1.0) +@export var text_hover_color: Color = Color(0.025, 0.12, 0.19, 1.0) +@export var text_pressed_color: Color = Color(0.025, 0.11, 0.17, 1.0) +@export var text_disabled_color: Color = Color(0.16, 0.25, 0.29, 0.72) + + +func make_normal_style() -> StyleBoxFlat: + return _make_style( + normal_fill, + normal_border, + normal_border_width + ) + + +func make_hover_style() -> StyleBoxFlat: + return _make_style( + hover_fill, + hover_border, + emphasized_border_width + ) + + +func make_pressed_style() -> StyleBoxFlat: + return _make_style( + pressed_fill, + pressed_border, + emphasized_border_width + ) + + +func make_disabled_style() -> StyleBoxFlat: + return _make_style( + disabled_fill, + disabled_border, + normal_border_width + ) + + +func _make_style( + fill_color: Color, + outline_color: Color, + outline_width: int, +) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.content_margin_left = content_margin + style.content_margin_top = content_margin + style.content_margin_right = content_margin + style.content_margin_bottom = content_margin + style.bg_color = fill_color + style.border_width_left = outline_width + style.border_width_top = outline_width + style.border_width_right = outline_width + style.border_width_bottom = outline_width + style.border_color = outline_color + style.corner_radius_top_left = corner_radius + style.corner_radius_top_right = corner_radius + style.corner_radius_bottom_right = corner_radius + style.corner_radius_bottom_left = corner_radius + return style diff --git a/ui/components/bubble_menu/bubble_menu_profile.gd.uid b/ui/components/bubble_menu/bubble_menu_profile.gd.uid new file mode 100644 index 0000000..e709b29 --- /dev/null +++ b/ui/components/bubble_menu/bubble_menu_profile.gd.uid @@ -0,0 +1 @@ +uid://ccliyvoivfimr diff --git a/ui/components/bubble_menu/bubble_menu_profile.tres b/ui/components/bubble_menu/bubble_menu_profile.tres new file mode 100644 index 0000000..c77b02c --- /dev/null +++ b/ui/components/bubble_menu/bubble_menu_profile.tres @@ -0,0 +1,6 @@ +[gd_resource type="Resource" script_class="BubbleMenuProfile" load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_menu_profile.gd" id="1_profile"] + +[resource] +script = ExtResource("1_profile") diff --git a/ui/title_screen.gd b/ui/title_screen.gd index 74a0552..46839ff 100644 --- a/ui/title_screen.gd +++ b/ui/title_screen.gd @@ -7,6 +7,12 @@ const SettingsManagerType = preload( "res://settings/player_settings_manager.gd" ) const SettingsPanelType = preload("res://ui/settings_panel.gd") +const BubbleButtonType = preload( + "res://ui/components/bubble_menu/bubble_button.gd" +) +const BubbleClusterType = preload( + "res://ui/components/bubble_menu/bubble_cluster.gd" +) const DECORATIVE_FISH_TEXTURES: Array[Texture2D] = [ preload("res://fish/species/bass/fish_bass_striped.png"), preload("res://fish/species/bluegill/fish_bluegill.png"), @@ -45,14 +51,19 @@ const BUBBLE_WOBBLE_MAX: float = 9.0 const BUBBLE_EDGE_MARGIN: float = 16.0 const MAX_DECORATIVE_BUBBLES: int = 6 const LOGO_ASPECT_RATIO: float = 2560.0 / 760.0 -const LOGO_WIDTH_FACTOR: float = 0.52 -const LOGO_MIN_WIDTH: float = 320.0 -const LOGO_MAX_WIDTH: float = 720.0 -const BUTTON_COLUMN_WIDTH: float = 360.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 BUBBLE_FIELD_MAX_WIDTH: float = 396.0 +const BUBBLE_FIELD_DESKTOP_HEIGHT: float = 318.0 +const BUBBLE_FIELD_COMPACT_WIDTH: float = 294.0 +const BUBBLE_FIELD_COMPACT_HEIGHT: float = 200.0 +const BUBBLE_COMPACT_HEIGHT_THRESHOLD: float = 560.0 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 signal gameplay_requested signal quit_requested @@ -63,11 +74,13 @@ enum ConfirmationAction { DELETE_SAVE, } -@onready var _continue_button: Button = %ContinueButton -@onready var _new_game_button: Button = %NewGameButton -@onready var _settings_button: Button = %SettingsButton -@onready var _delete_button: Button = %DeleteSaveButton -@onready var _quit_button: Button = %QuitButton +@onready var _continue_button: BubbleButtonType = %ContinueButton +@onready var _new_game_button: BubbleButtonType = %NewGameButton +@onready var _settings_button: BubbleButtonType = %SettingsButton +@onready var _delete_button: BubbleButtonType = %DeleteSaveButton +@onready var _quit_button: BubbleButtonType = %QuitButton +@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 @@ -81,7 +94,7 @@ enum ConfirmationAction { @onready var _decorative_bubble_cluster_timer: Timer = %DecorativeBubbleClusterTimer @onready var _title_logo: TextureRect = %TitleLogo @onready var _button_center: CenterContainer = %ButtonCenter -@onready var _button_stack: VBoxContainer = %ButtonStack +@onready var _bubble_field: BubbleClusterType = %BubbleField @onready var _start_prompt_center: CenterContainer = %StartPromptCenter @onready var _start_prompt_label: Label = %StartPromptLabel @@ -116,6 +129,7 @@ func _ready() -> void: %CancelConfirmButton.pressed.connect(_close_confirmation) _settings_panel.applied.connect(_on_settings_applied) _settings_panel.closed.connect(_on_settings_closed) + _bubble_field.configure(_get_title_buttons()) _decorative_fish_timer.timeout.connect(_on_decorative_fish_timer_timeout) _decorative_bubble_event_timer.timeout.connect( _on_decorative_bubble_event_timer_timeout @@ -199,13 +213,37 @@ func _update_title_layout() -> void: logo_width, logo_width / LOGO_ASPECT_RATIO ) - _button_stack.custom_minimum_size = Vector2( - minf(BUTTON_COLUMN_WIDTH, available_width), - 0.0 + 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 + else BUBBLE_FIELD_DESKTOP_HEIGHT + ) + if size.y < BUBBLE_COMPACT_HEIGHT_THRESHOLD: + field_width = minf(BUBBLE_FIELD_COMPACT_WIDTH, available_width) + _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 + ) + _new_game_label.text = "new\ngame" if compact_layout else "new game" + _delete_save_label.text = ( + "delete\nsave" if compact_layout else "delete save" ) _update_world_preview_resolution() +func _get_title_buttons() -> Array[BubbleButton]: + return [ + _continue_button, + _new_game_button, + _settings_button, + _delete_button, + _quit_button, + ] + + func set_world_pixelation(pixel_size: int) -> void: _world_pixel_size = clampi( pixel_size, @@ -254,24 +292,27 @@ func _snap_world_preview(value: Vector2) -> Vector2: func _process(delta: float) -> void: - if ( - not _awaiting_start_input - or not visible - or not _start_prompt_center.visible - ): + if not visible: return - _start_prompt_elapsed = fmod( - _start_prompt_elapsed + delta, - START_PROMPT_CYCLE_SECONDS - ) - var phase: float = _start_prompt_elapsed / START_PROMPT_CYCLE_SECONDS - var pulse_weight: float = (sin(phase * TAU - PI * 0.5) + 1.0) * 0.5 - var prompt_scale: float = lerpf( - START_PROMPT_MIN_SCALE, - START_PROMPT_MAX_SCALE, - pulse_weight - ) - _start_prompt_label.scale = Vector2.ONE * prompt_scale + if _awaiting_start_input and _start_prompt_center.visible: + _start_prompt_elapsed = fmod( + _start_prompt_elapsed + delta, + START_PROMPT_CYCLE_SECONDS + ) + var phase: float = ( + _start_prompt_elapsed / START_PROMPT_CYCLE_SECONDS + ) + var pulse_weight: float = ( + sin(phase * TAU - PI * 0.5) + 1.0 + ) * 0.5 + var prompt_scale: float = lerpf( + START_PROMPT_MIN_SCALE, + START_PROMPT_MAX_SCALE, + pulse_weight + ) + _start_prompt_label.scale = Vector2.ONE * prompt_scale + if _button_center.visible: + _bubble_field.advance_motion(delta) func _input(event: InputEvent) -> void: @@ -387,7 +428,7 @@ func _start_entry_prompt_animation() -> void: func _stop_entry_prompt_animation() -> void: - set_process(false) + set_process(visible and _button_center.visible) _start_prompt_elapsed = 0.0 _start_prompt_label.scale = Vector2.ONE @@ -406,6 +447,7 @@ func _reveal_primary_menu() -> void: _start_prompt_center.hide() _button_center.show() _feedback_label.show() + set_process(true) _navigation_focus_active = false _release_title_focus() @@ -556,6 +598,9 @@ func _refresh_save_inspection() -> void: _inspection = _save_manager.inspect_save() _continue_button.disabled = not _inspection.can_continue() _delete_button.disabled = not _inspection.can_delete() + _delete_save_label.modulate.a = ( + DISABLED_BUBBLE_LABEL_ALPHA if _delete_button.disabled else 1.0 + ) _feedback_label.text = _inspection.message if _inspection.status == SaveInspectionType.Status.VALID_SUPPORTED: _feedback_label.text = ( @@ -596,6 +641,7 @@ func _on_title_visibility_changed() -> void: if not _decorative_presentation_ready: return if visible: + set_process(true) _start_decorative_presentation() else: _stop_entry_prompt_animation() diff --git a/ui/title_screen.tscn b/ui/title_screen.tscn index 6df2b94..ac5e1ef 100644 --- a/ui/title_screen.tscn +++ b/ui/title_screen.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=9 format=3] +[gd_scene load_steps=12 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"] @@ -6,6 +6,9 @@ [ext_resource type="Shader" path="res://ui/title_water_background.gdshader" id="4_water_shader"] [ext_resource type="Texture2D" path="res://ui/assets/title/netfishing_logo.png" id="5_logo"] [ext_resource type="Shader" path="res://ui/title_logo_underwater.gdshader" id="6_logo_shader"] +[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"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water"] shader = ExtResource("4_water_shader") @@ -109,7 +112,7 @@ grow_vertical = 2 [node name="MainContent" type="VBoxContainer" parent="Center"] layout_mode = 2 -theme_override_constants/separation = 8 +theme_override_constants/separation = 7 alignment = 1 [node name="TitleLogo" type="TextureRect" parent="Center/MainContent"] @@ -126,12 +129,12 @@ stretch_mode = 5 [node name="PlaytestLabel" type="Label" parent="Center/MainContent"] layout_mode = 2 theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1) -theme_override_font_sizes/font_size = 13 +theme_override_font_sizes/font_size = 17 text = "pre-alpha playtest 0.2" horizontal_alignment = 1 [node name="Spacer" type="Control" parent="Center/MainContent"] -custom_minimum_size = Vector2(0, 4) +custom_minimum_size = Vector2(0, 3) layout_mode = 2 [node name="ButtonCenter" type="CenterContainer" parent="Center/MainContent"] @@ -139,46 +142,151 @@ unique_name_in_owner = true visible = false layout_mode = 2 -[node name="ButtonStack" type="VBoxContainer" parent="Center/MainContent/ButtonCenter"] +[node name="BubbleField" type="Control" parent="Center/MainContent/ButtonCenter"] unique_name_in_owner = true -custom_minimum_size = Vector2(360, 0) +custom_minimum_size = Vector2(396, 318) layout_mode = 2 -theme_override_constants/separation = 8 +script = ExtResource("8_bubble_cluster") +profile = ExtResource("9_bubble_profile") -[node name="ContinueButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] +[node name="ContinueButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleField"] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 44) -layout_mode = 2 +offset_left = 123.0 +offset_top = 86.0 +offset_right = 307.0 +offset_bottom = 264.0 text = "continue" +script = ExtResource("7_bubble_button") +profile = ExtResource("9_bubble_profile") +neutral_size = Vector2(184, 178) +desktop_anchor = Vector2(215, 175) +compact_anchor = Vector2(215, 175) +minimum_font_size = 19 +maximum_font_size = 30 +horizontal_amplitude = 1.8 +vertical_amplitude = 4.6 +motion_period = 5.8 +deformation_amplitude = 0.01 +deformation_period = 6.7 -[node name="NewGameButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] +[node name="NewGameButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleField"] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 44) -layout_mode = 2 +offset_left = 42.0 +offset_top = 4.0 +offset_right = 170.0 +offset_bottom = 126.0 +script = ExtResource("7_bubble_button") +profile = ExtResource("9_bubble_profile") +neutral_size = Vector2(128, 122) +desktop_anchor = Vector2(106, 65) +compact_anchor = Vector2(106, 68) +compact_minimum_size = Vector2(84, 84) +label_control_path = NodePath("NewGameLabel") +minimum_font_size = 15 +maximum_font_size = 23 +horizontal_amplitude = 2.3 +vertical_amplitude = 5.5 +motion_period = 6.3 +motion_phase = 1.2 +deformation_amplitude = 0.018 +deformation_period = 5.9 + +[node name="NewGameLabel" type="Label" parent="Center/MainContent/ButtonCenter/BubbleField/NewGameButton"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) +theme_override_constants/line_spacing = -3 text = "new game" +horizontal_alignment = 1 +vertical_alignment = 1 -[node name="SettingsButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] +[node name="SettingsButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleField"] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 44) -layout_mode = 2 +offset_left = 264.0 +offset_top = 17.0 +offset_right = 384.0 +offset_bottom = 133.0 text = "settings" +script = ExtResource("7_bubble_button") +profile = ExtResource("9_bubble_profile") +desktop_anchor = Vector2(324, 75) +compact_anchor = Vector2(324, 68) +compact_minimum_size = Vector2(94, 90) +minimum_font_size = 15 +maximum_font_size = 22 +vertical_amplitude = 3.7 +motion_period = 4.9 +motion_phase = 2.5 +deformation_period = 6.3 -[node name="DeleteSaveButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] +[node name="DeleteSaveButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleField"] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 44) -layout_mode = 2 +offset_left = -11.0 +offset_top = 122.0 +offset_right = 123.0 +offset_bottom = 248.0 +script = ExtResource("7_bubble_button") +profile = ExtResource("9_bubble_profile") +neutral_size = Vector2(134, 126) +desktop_anchor = Vector2(56, 185) +compact_anchor = Vector2(53, 196) +compact_minimum_size = Vector2(86, 86) +label_control_path = NodePath("DeleteSaveLabel") +minimum_font_size = 15 +maximum_font_size = 23 +vertical_amplitude = 5.1 +motion_period = 5.5 +motion_phase = 3.7 +deformation_amplitude = 0.018 +deformation_period = 5.4 + +[node name="DeleteSaveLabel" type="Label" parent="Center/MainContent/ButtonCenter/BubbleField/DeleteSaveButton"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) +theme_override_constants/line_spacing = -3 text = "delete save" +horizontal_alignment = 1 +vertical_alignment = 1 -[node name="QuitButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] +[node name="QuitButton" type="Button" parent="Center/MainContent/ButtonCenter/BubbleField"] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 44) -layout_mode = 2 +offset_left = 275.0 +offset_top = 217.0 +offset_right = 369.0 +offset_bottom = 309.0 text = "quit" +script = ExtResource("7_bubble_button") +profile = ExtResource("9_bubble_profile") +neutral_size = Vector2(94, 92) +desktop_anchor = Vector2(322, 263) +compact_anchor = Vector2(322, 263) +compact_minimum_size = Vector2(68, 68) +minimum_font_size = 14 +maximum_font_size = 18 +horizontal_amplitude = 1.4 +vertical_amplitude = 3.2 +motion_period = 4.2 +motion_phase = 4.8 +deformation_amplitude = 0.02 +deformation_period = 4.8 [node name="FeedbackLabel" type="Label" parent="Center/MainContent"] unique_name_in_owner = true visible = false -custom_minimum_size = Vector2(0, 42) +custom_minimum_size = Vector2(0, 44) layout_mode = 2 text = "" horizontal_alignment = 1