Make bubble menu feedback immediate

This commit is contained in:
Alexander Sellite 2026-08-22 22:56:44 -04:00
parent 8f8398cbcd
commit 3dbfd74b6c
6 changed files with 214 additions and 238 deletions

View file

@ -67,13 +67,7 @@ const BUBBLE_WOBBLE_MIN: float = 3.0
const BUBBLE_WOBBLE_MAX: float = 9.0
const BUBBLE_EDGE_MARGIN: float = 16.0
const MAX_DECORATIVE_BUBBLES: int = 6
const TRANSITION_BUBBLE_COUNT_MIN: int = 4
const TRANSITION_BUBBLE_COUNT_MAX: int = 5
const MAX_TRANSITION_BUBBLES: int = 30
const TRANSITION_BUBBLE_X_MIN: float = 0.36
const TRANSITION_BUBBLE_X_MAX: float = 0.64
const TRANSITION_BUBBLE_Y_MIN: float = 0.60
const TRANSITION_BUBBLE_Y_MAX: float = 0.78
const LOGO_ASPECT_RATIO: float = 2560.0 / 760.0
const LOGO_WIDTH_FACTOR: float = 0.4784
const LOGO_MIN_WIDTH: float = 294.0
@ -89,11 +83,9 @@ 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 INTRO_PROMPT_FADE_DURATION: float = 0.55
const INTRO_BUBBLE_TRAVEL_DURATION: float = 2.40
const INTRO_BRANDING_TRAVEL_DURATION: float = 2.0
const INTRO_BRANDING_START_DELAY: float = 0.35
const HOST_CLUSTER_SAFE_MARGIN: float = 24.0
const QUICK_MENU_ENTER_DURATION: float = 0.14
const QUICK_MENU_EXIT_DURATION: float = 0.10
const QUICK_MENU_ENTER_SCALE: float = 0.97
signal new_game_requested(world_layout: StringName, world_seed: int)
signal continue_game_requested
@ -128,13 +120,11 @@ enum ConfirmationAction {
@onready var _decorative_bubble_cluster_timer: Timer = %DecorativeBubbleClusterTimer
@onready var _title_logo: TextureRect = %TitleLogo
@onready var _playtest_label: Label = %PlaytestLabel
@onready var _intro_branding_anchor: Control = %IntroBrandingAnchor
@onready var _presentation_center: CenterContainer = %Center
@onready var _main_content: VBoxContainer = %MainContent
@onready var _branding_slot: Control = %BrandingSlot
@onready var _branding_motion_root: Control = %BrandingMotionRoot
@onready var _version_row: Control = %VersionRow
@onready var _title_spacer: Control = %Spacer
@onready var _button_center: CenterContainer = %ButtonCenter
@onready var _bubble_layout_slot: Control = %BubbleLayoutSlot
@onready var _bubble_motion_root: Control = %BubbleMotionRoot
@ -176,8 +166,6 @@ var _title_entry_generation: int = 0
var _title_entry_transition_active: bool = false
var _intro_geometry_ready: bool = false
var _intro_layout_generation: int = 0
var _branding_intro_position: Vector2 = Vector2.ZERO
var _bubble_intro_position: Vector2 = Vector2.ZERO
var _feedback_visible_before_settings: bool = false
var _feedback_text_before_settings: String = ""
var _continue_stats_hovered: bool = false
@ -227,6 +215,7 @@ func _ready() -> void:
)
_credits_page.back_requested.connect(_close_credits)
_bubble_field.configure(_get_title_buttons())
_bubble_field.motion_scale = 0.0
_decorative_fish_timer.timeout.connect(_on_decorative_fish_timer_timeout)
_decorative_bubble_event_timer.timeout.connect(
_on_decorative_bubble_event_timer_timeout
@ -757,28 +746,30 @@ func _reveal_primary_menu() -> void:
set_process(true)
_navigation_focus_active = false
_release_title_focus()
_title_entry_transition = create_tween()
_title_entry_transition.set_parallel(true)
_bubble_motion_root.position = Vector2.ZERO
_branding_motion_root.position = Vector2.ZERO
_bubble_field.pivot_offset = _bubble_field.size * 0.5
_bubble_field.scale = Vector2.ONE * QUICK_MENU_ENTER_SCALE
_bubble_field.modulate.a = 0.0
_title_entry_transition = create_tween().set_parallel(true)
_title_entry_transition.tween_property(
_start_prompt_center,
"modulate:a",
0.0,
INTRO_PROMPT_FADE_DURATION
).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT)
QUICK_MENU_EXIT_DURATION
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
_title_entry_transition.tween_property(
_bubble_motion_root,
"position",
Vector2.ZERO,
INTRO_BUBBLE_TRAVEL_DURATION
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_bubble_field,
"scale",
Vector2.ONE,
QUICK_MENU_ENTER_DURATION
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
_title_entry_transition.tween_property(
_branding_motion_root,
"position",
Vector2.ZERO,
INTRO_BRANDING_TRAVEL_DURATION
).set_delay(INTRO_BRANDING_START_DELAY).set_trans(
Tween.TRANS_QUAD
).set_ease(Tween.EASE_IN_OUT)
_bubble_field,
"modulate:a",
1.0,
QUICK_MENU_ENTER_DURATION
)
_title_entry_transition.finished.connect(
_finish_primary_menu_reveal.bind(generation),
CONNECT_ONE_SHOT
@ -795,6 +786,8 @@ func _finish_primary_menu_reveal(generation: int) -> void:
_title_entry_transition_active = false
_start_prompt_center.hide()
_start_prompt_center.modulate.a = 1.0
_bubble_field.scale = Vector2.ONE
_bubble_field.modulate.a = 1.0
_feedback_label.modulate.a = 0.0
_set_title_bubbles_interactive(true)
@ -807,6 +800,10 @@ func _cancel_title_entry_transition() -> void:
_title_entry_transition_active = false
if is_node_ready():
_start_prompt_center.modulate.a = 1.0
_bubble_motion_root.position = Vector2.ZERO
_branding_motion_root.position = Vector2.ZERO
_bubble_field.scale = Vector2.ONE
_bubble_field.modulate.a = 1.0
func _schedule_intro_presentation() -> void:
@ -830,59 +827,13 @@ func _prepare_intro_presentation(generation: int) -> void:
return
_branding_motion_root.size = _branding_slot.size
_bubble_motion_root.size = _bubble_layout_slot.size
var intro_tail_height: float = (
_title_spacer.get_combined_minimum_size().y
+ float(_main_content.get_theme_constant("separation"))
)
var inverse_stage_transform: Transform2D = (
_title_presentation_scale_root.get_global_transform().affine_inverse()
)
var branding_anchor_position: Vector2 = (
inverse_stage_transform
* _intro_branding_anchor.get_global_transform().origin
)
var branding_slot_position: Vector2 = (
inverse_stage_transform
* _branding_slot.get_global_transform().origin
)
var branding_target_position := Vector2(
floorf(
branding_anchor_position.x
- _branding_motion_root.size.x * 0.5
),
floorf(
branding_anchor_position.y
- (_branding_motion_root.size.y + intro_tail_height) * 0.5
)
)
_branding_intro_position = (
branding_target_position
- branding_slot_position
)
var bubble_bounds: Rect2 = _get_title_bubble_stage_bounds()
bubble_bounds.position -= _bubble_motion_root.position
var presentation_allowance: float = _get_bubble_offscreen_allowance()
_bubble_intro_position = Vector2(
0.0,
_title_presentation_scale_root.size.y
+ presentation_allowance
- bubble_bounds.position.y
)
_branding_motion_root.position = _branding_intro_position
_bubble_motion_root.position = _bubble_intro_position
_branding_motion_root.position = Vector2.ZERO
_bubble_motion_root.position = Vector2.ZERO
_title_content_rest_position = _presentation_center.position
_title_bubble_rest_position = _bubble_field.position
_intro_geometry_ready = true
func _get_title_bubble_stage_bounds() -> Rect2:
var bubbles: Array[BubbleButton] = _get_title_buttons()
var bounds: Rect2 = _get_title_stage_rect(bubbles[0])
for index: int in range(1, bubbles.size()):
bounds = bounds.merge(_get_title_stage_rect(bubbles[index]))
return bounds
func _get_title_stage_rect(control: Control) -> Rect2:
var inverse_stage_transform: Transform2D = (
_title_presentation_scale_root.get_global_transform().affine_inverse()
@ -895,30 +846,6 @@ func _get_title_stage_rect(control: Control) -> Rect2:
return Rect2(local_position, local_end - local_position)
func _get_bubble_offscreen_allowance() -> float:
var maximum_vertical_amplitude: float = 0.0
var maximum_deformation_growth: float = 0.0
for bubble: BubbleButton in _get_title_buttons():
maximum_vertical_amplitude = maxf(
maximum_vertical_amplitude,
bubble.vertical_amplitude
)
var maximum_scale: float = (
1.0 + bubble.deformation_amplitude
) * _bubble_field.profile.hover_focus_scale
maximum_deformation_growth = maxf(
maximum_deformation_growth,
bubble.presented_size.y * (maximum_scale - 1.0) * 0.5
)
return (
HOST_CLUSTER_SAFE_MARGIN
+ maximum_vertical_amplitude
+ maximum_deformation_growth
+ _bubble_field.profile.hover_focus_lift
+ _bubble_field.profile.maximum_separation
)
func _on_continue_stats_hover_changed(hovered: bool) -> void:
_continue_stats_hovered = hovered
_update_continue_stats_visibility()
@ -975,7 +902,7 @@ func _fade_continue_stats_to(target_opacity: float) -> void:
_feedback_label,
"modulate:a",
target_opacity,
INTRO_PROMPT_FADE_DURATION
QUICK_MENU_ENTER_DURATION
).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT)
@ -1176,17 +1103,20 @@ func _begin_confirmation_open() -> void:
_set_title_bubbles_interactive(false)
_release_primary_menu_focus()
var generation: int = _confirmation_transition_generation
var content_bounds: Rect2 = _get_title_stage_rect(_main_content)
var exit_distance: float = (
content_bounds.end.y + HOST_CLUSTER_SAFE_MARGIN
)
_confirmation_transition = create_tween()
_presentation_center.pivot_offset = _presentation_center.size * 0.5
_confirmation_transition = create_tween().set_parallel(true)
_confirmation_transition.tween_property(
_presentation_center,
"position:y",
_confirmation_title_content_rest_position.y - exit_distance,
UIMotion.bubble_duration(exit_distance)
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
"scale",
Vector2.ONE * QUICK_MENU_ENTER_SCALE,
QUICK_MENU_EXIT_DURATION
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
_confirmation_transition.tween_property(
_presentation_center,
"modulate:a",
0.0,
QUICK_MENU_EXIT_DURATION
)
_confirmation_transition.finished.connect(
_finish_confirmation_title_exit.bind(generation),
CONNECT_ONE_SHOT
@ -1202,8 +1132,9 @@ func _finish_confirmation_title_exit(generation: int) -> void:
_confirmation_transition = null
_presentation_center.hide()
_presentation_center.position = _confirmation_title_content_rest_position
_reset_quick_menu_presentation(_presentation_center)
_confirmation_page.transition_in(
0.0,
QUICK_MENU_ENTER_DURATION,
_finish_confirmation_open.bind(generation)
)
@ -1227,7 +1158,7 @@ func _begin_confirmation_return() -> void:
_confirmation_page.lock_interaction()
var generation: int = _confirmation_transition_generation
_confirmation_page.transition_out(
0.0,
QUICK_MENU_EXIT_DURATION,
_finish_confirmation_page_exit.bind(generation)
)
@ -1239,23 +1170,22 @@ func _finish_confirmation_page_exit(generation: int) -> void:
):
return
_confirmation_action = ConfirmationAction.NONE
_presentation_center.position = Vector2(
_confirmation_title_content_rest_position.x,
_title_presentation_scale_root.size.y
+ HOST_CLUSTER_SAFE_MARGIN
)
_presentation_center.position = _confirmation_title_content_rest_position
_presentation_center.show()
var entry_distance: float = absf(
_presentation_center.position.y
- _confirmation_title_content_rest_position.y
)
_confirmation_transition = create_tween()
_prepare_quick_menu_enter(_presentation_center)
_confirmation_transition = create_tween().set_parallel(true)
_confirmation_transition.tween_property(
_presentation_center,
"position",
_confirmation_title_content_rest_position,
UIMotion.bubble_duration(entry_distance)
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
"scale",
Vector2.ONE,
QUICK_MENU_ENTER_DURATION
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
_confirmation_transition.tween_property(
_presentation_center,
"modulate:a",
1.0,
QUICK_MENU_ENTER_DURATION
)
_confirmation_transition.finished.connect(
_finish_confirmation_return.bind(generation),
CONNECT_ONE_SHOT
@ -1271,6 +1201,7 @@ func _finish_confirmation_return(generation: int) -> void:
_confirmation_transition = null
_confirmation_transition_active = false
_presentation_center.position = _confirmation_title_content_rest_position
_reset_quick_menu_presentation(_presentation_center)
_set_title_bubbles_interactive(true)
var restore_navigation_focus: bool = _modal_restore_navigation_focus
_modal_restore_navigation_focus = false
@ -1297,6 +1228,7 @@ func _reset_confirmation() -> void:
_confirmation_page.hide_page()
if is_node_ready():
_presentation_center.position = _confirmation_title_content_rest_position
_reset_quick_menu_presentation(_presentation_center)
_presentation_center.show()
@ -1364,17 +1296,20 @@ func _begin_title_cluster_exit() -> void:
_set_title_bubbles_interactive(false)
_release_primary_menu_focus()
var generation: int = _title_settings_transition_generation
var content_bounds: Rect2 = _get_title_stage_rect(_main_content)
var exit_distance: float = (
content_bounds.end.y + HOST_CLUSTER_SAFE_MARGIN
)
_title_settings_transition = create_tween()
_presentation_center.pivot_offset = _presentation_center.size * 0.5
_title_settings_transition = create_tween().set_parallel(true)
_title_settings_transition.tween_property(
_presentation_center,
"position:y",
_title_content_rest_position.y - exit_distance,
UIMotion.bubble_duration(exit_distance)
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
"scale",
Vector2.ONE * QUICK_MENU_ENTER_SCALE,
QUICK_MENU_EXIT_DURATION
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
_title_settings_transition.tween_property(
_presentation_center,
"modulate:a",
0.0,
QUICK_MENU_EXIT_DURATION
)
_title_settings_transition.finished.connect(
_finish_title_cluster_exit.bind(generation),
CONNECT_ONE_SHOT
@ -1402,6 +1337,7 @@ func _finish_title_cluster_exit(generation: int) -> void:
_title_settings_transition_active = false
_presentation_center.hide()
_presentation_center.position = _title_content_rest_position
_reset_quick_menu_presentation(_presentation_center)
func _begin_title_cluster_return(feedback_text: String) -> void:
@ -1415,21 +1351,22 @@ func _begin_title_cluster_return(feedback_text: String) -> void:
_hide_continue_stats_context()
_set_title_bubbles_interactive(false)
var generation: int = _title_settings_transition_generation
_presentation_center.position = Vector2(
_title_content_rest_position.x,
_title_presentation_scale_root.size.y + HOST_CLUSTER_SAFE_MARGIN
)
_presentation_center.position = _title_content_rest_position
_presentation_center.show()
var entry_distance: float = absf(
_presentation_center.position.y - _title_content_rest_position.y
)
_title_settings_transition = create_tween()
_prepare_quick_menu_enter(_presentation_center)
_title_settings_transition = create_tween().set_parallel(true)
_title_settings_transition.tween_property(
_presentation_center,
"position",
_title_content_rest_position,
UIMotion.bubble_duration(entry_distance)
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
"scale",
Vector2.ONE,
QUICK_MENU_ENTER_DURATION
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
_title_settings_transition.tween_property(
_presentation_center,
"modulate:a",
1.0,
QUICK_MENU_ENTER_DURATION
)
_title_settings_transition.finished.connect(
_finish_title_cluster_return.bind(generation),
CONNECT_ONE_SHOT
@ -1445,6 +1382,7 @@ func _finish_title_cluster_return(generation: int) -> void:
_title_settings_transition = null
_title_settings_transition_active = false
_presentation_center.position = _title_content_rest_position
_reset_quick_menu_presentation(_presentation_center)
_bubble_field.position = _title_bubble_rest_position
_set_title_bubbles_interactive(true)
_restore_settings_focus()
@ -1493,6 +1431,7 @@ func _cancel_title_settings_transition() -> void:
_title_settings_transition_active = false
if is_node_ready():
_presentation_center.position = _title_content_rest_position
_reset_quick_menu_presentation(_presentation_center)
_presentation_center.show()
_bubble_field.position = _title_bubble_rest_position
_set_title_bubbles_interactive(true)
@ -1504,6 +1443,17 @@ func _cancel_title_settings_tween() -> void:
_title_settings_transition = null
func _prepare_quick_menu_enter(control: Control) -> void:
control.pivot_offset = control.size * 0.5
control.scale = Vector2.ONE * QUICK_MENU_ENTER_SCALE
control.modulate.a = 0.0
func _reset_quick_menu_presentation(control: Control) -> void:
control.scale = Vector2.ONE
control.modulate.a = 1.0
func _restore_settings_focus() -> void:
var restore_navigation_focus: bool = _modal_restore_navigation_focus
_modal_restore_navigation_focus = false
@ -2042,30 +1992,8 @@ func _get_idle_decorative_bubble_count() -> int:
func _emit_navigation_bubble_flurry() -> void:
if (
_minimal_presentation
or not _decorative_presentation_active
or not visible
):
return
var bubble_count: int = _decorative_rng.randi_range(
TRANSITION_BUBBLE_COUNT_MIN,
TRANSITION_BUBBLE_COUNT_MAX
)
for _bubble_index: int in bubble_count:
if not _spawn_decorative_bubble(
_decorative_generation,
_decorative_rng.randf_range(
TRANSITION_BUBBLE_Y_MIN,
TRANSITION_BUBBLE_Y_MAX
),
_decorative_rng.randf_range(
TRANSITION_BUBBLE_X_MIN,
TRANSITION_BUBBLE_X_MAX
),
true
):
break
# Page changes now use a quick fade and scale without travel bubbles.
pass
func _exit_tree() -> void: