Extract reusable bubble menu components

This commit is contained in:
Alexander Sellite 2026-07-27 14:42:57 -04:00
parent c7504d2585
commit 053cfe037d
11 changed files with 646 additions and 54 deletions

View file

@ -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.

View file

@ -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)

View file

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

View file

@ -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")

View file

@ -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
)

View file

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

View file

@ -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

View file

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

View file

@ -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")

View file

@ -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()

View file

@ -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