Extract reusable bubble menu components
This commit is contained in:
parent
c7504d2585
commit
053cfe037d
11 changed files with 646 additions and 54 deletions
26
ui/components/bubble_menu/README.md
Normal file
26
ui/components/bubble_menu/README.md
Normal 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.
|
||||
186
ui/components/bubble_menu/bubble_button.gd
Normal file
186
ui/components/bubble_menu/bubble_button.gd
Normal 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)
|
||||
1
ui/components/bubble_menu/bubble_button.gd.uid
Normal file
1
ui/components/bubble_menu/bubble_button.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d15kke1iwoueg
|
||||
9
ui/components/bubble_menu/bubble_button.tscn
Normal file
9
ui/components/bubble_menu/bubble_button.tscn
Normal 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")
|
||||
117
ui/components/bubble_menu/bubble_cluster.gd
Normal file
117
ui/components/bubble_menu/bubble_cluster.gd
Normal 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
|
||||
)
|
||||
1
ui/components/bubble_menu/bubble_cluster.gd.uid
Normal file
1
ui/components/bubble_menu/bubble_cluster.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cvqif55pw6r32
|
||||
91
ui/components/bubble_menu/bubble_menu_profile.gd
Normal file
91
ui/components/bubble_menu/bubble_menu_profile.gd
Normal 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
|
||||
1
ui/components/bubble_menu/bubble_menu_profile.gd.uid
Normal file
1
ui/components/bubble_menu/bubble_menu_profile.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ccliyvoivfimr
|
||||
6
ui/components/bubble_menu/bubble_menu_profile.tres
Normal file
6
ui/components/bubble_menu/bubble_menu_profile.tres
Normal 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")
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue