class_name TitleScreen extends Control const SaveInspectionType = preload("res://save/player_save_inspection.gd") const SaveManagerType = preload("res://save/player_save_manager.gd") const SettingsManagerType = preload( "res://settings/player_settings_manager.gd" ) const SettingsPanelType = preload("res://ui/settings_panel.gd") const FullscreenMenuPresentationType = preload( "res://ui/fullscreen_menu_presentation.gd" ) const ControllerFocusNavigationType = preload( "res://ui/controller_focus_navigation.gd" ) const BubbleButtonType = preload( "res://ui/components/bubble_menu/bubble_button.gd" ) const BubbleClusterType = preload( "res://ui/components/bubble_menu/bubble_cluster.gd" ) const TitleConfirmationBubblePageType = preload( "res://ui/title_confirmation_bubble_page.gd" ) const TitleCreditsPageType = preload("res://ui/title_credits_page.gd") const NetworkSessionType = preload("res://network/network_session.gd") const SavedServerStoreType = preload("res://network/saved_server_store.gd") const JoinGamePageType = preload("res://ui/network/join_game_page.gd") const NewGameSetupPageType = preload("res://ui/new_game_setup_page.gd") const SaveSlotsPageType = preload("res://ui/save_slots_page.gd") const CurrencyPresentationType = preload( "res://ui/currency_presentation.gd" ) const LOGO_ASPECT_RATIO: float = 2560.0 / 480.0 const LOGO_WIDTH_FACTOR: float = 0.4784 const LOGO_MIN_WIDTH: float = 294.0 const LOGO_MAX_WIDTH: float = 662.0 const TITLE_HORIZONTAL_MARGIN: float = 48.0 const TITLE_DESKTOP_REFERENCE_SIZE: Vector2 = Vector2(1280.0, 720.0) const TITLE_COMPACT_REFERENCE_SIZE: Vector2 = Vector2(640.0, 480.0) const BUBBLE_FIELD_MAX_WIDTH: float = 500.0 const BUBBLE_FIELD_DESKTOP_HEIGHT: float = 360.0 const BUBBLE_FIELD_COMPACT_WIDTH: float = 294.0 const BUBBLE_FIELD_COMPACT_HEIGHT: float = 200.0 const BUBBLE_FIELD_DESKTOP_OFFSET_Y: float = -10.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 QUICK_MENU_ENTER_DURATION: float = 0.14 const QUICK_MENU_EXIT_DURATION: float = 0.10 const QUICK_MENU_ENTER_SCALE: float = 0.97 const INTRO_PROMPT_FADE_DURATION: float = 0.24 const INTRO_LOGO_FLOAT_DURATION: float = 0.72 const INTRO_MENU_REVEAL_DELAY: float = 0.34 const INTRO_MENU_REVEAL_DURATION: float = 0.30 const INTRO_LOGO_CENTER_Y_RATIO: float = 0.43 signal new_game_requested(world_layout: StringName, world_seed: int) signal continue_game_requested signal slot_play_requested(slot_id: String) signal new_slot_requested( display_name: String, world_layout: StringName, world_seed: int, duplicate_source_slot_id: String, ) signal quit_requested signal join_game_requested(endpoint: String) enum ConfirmationAction { NONE, DELETE_SAVE, } @onready var _play_button: BubbleButtonType = %PlayButton @onready var _new_game_button: BubbleButtonType = %NewGameButton @onready var _settings_button: BubbleButtonType = %SettingsButton @onready var _credits_button: BubbleButtonType = %CreditsButton @onready var _delete_button: BubbleButtonType = %DeleteSaveButton @onready var _quit_button: BubbleButtonType = %QuitButton @onready var _join_game_button: BubbleButtonType = %JoinGameButton @onready var _feedback_label: RichTextLabel = %FeedbackLabel @onready var _continue_stats_drawer: RichTextLabel = %ContinueStatsDrawer @onready var _confirmation_page: TitleConfirmationBubblePageType = ( %ConfirmationPage ) @onready var _settings_panel: SettingsPanelType = %SettingsPanel @onready var _title_presentation_scale_root: Control = ( %TitlePresentationScaleRoot ) @onready var _background: ColorRect = %Background @onready var _title_logo: TextureRect = %TitleLogo @onready var _playtest_label: Label = %PlaytestLabel @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 _button_center: CenterContainer = %ButtonCenter @onready var _bubble_layout_slot: Control = %BubbleLayoutSlot @onready var _bubble_motion_root: Control = %BubbleMotionRoot @onready var _bubble_field: BubbleClusterType = %BubbleField @onready var _start_prompt_center: CenterContainer = %StartPromptCenter @onready var _start_prompt_label: Label = %StartPromptLabel @onready var _join_game_page: JoinGamePageType = %JoinGamePage @onready var _new_game_setup_page: NewGameSetupPageType = %NewGameSetupPage @onready var _credits_page: TitleCreditsPageType = %CreditsPage @onready var _save_slots_page: SaveSlotsPageType = %SaveSlotsPage var _save_manager: SaveManagerType var _settings_manager: SettingsManagerType var _inspection: SaveInspectionType var _confirmation_action: ConfirmationAction = ConfirmationAction.NONE var _action_in_progress: bool = false var _awaiting_start_input: bool = false var _start_prompt_elapsed: float = 0.0 var _navigation_focus_active: bool = false var _modal_restore_navigation_focus: bool = false var _world_pixel_size: int = PlayerSettings.DEFAULT_WORLD_PIXEL_SIZE var _title_settings_transition: Tween var _title_settings_transition_generation: int = 0 var _title_settings_transition_active: bool = false var _title_bubble_rest_position: Vector2 = Vector2.ZERO var _title_content_rest_position: Vector2 = Vector2.ZERO var _title_entry_transition: Tween 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 _feedback_visible_before_settings: bool = false var _feedback_text_before_settings: String = "" var _continue_stats_hovered: bool = false var _continue_stats_focused: bool = false var _continue_stats_fade: Tween var _confirmation_transition: Tween var _confirmation_transition_generation: int = 0 var _confirmation_transition_active: bool = false var _confirmation_title_content_rest_position: Vector2 = Vector2.ZERO func _ready() -> void: var release_version: String = str( ProjectSettings.get_setting("application/config/version", "") ).strip_edges() if not release_version.is_empty(): _playtest_label.text = "v%s" % release_version _play_button.pressed.connect(_on_continue_pressed) _play_button.mouse_entered.connect( _on_continue_stats_hover_changed.bind(true) ) _play_button.mouse_exited.connect( _on_continue_stats_hover_changed.bind(false) ) _play_button.focus_entered.connect( _on_continue_stats_focus_changed.bind(true) ) _play_button.focus_exited.connect( _on_continue_stats_focus_changed.bind(false) ) _new_game_button.pressed.connect(_on_new_game_pressed) _join_game_button.pressed.connect(_open_join_game) _new_game_setup_page.start_requested.connect(_start_configured_new_game) _new_game_setup_page.back_requested.connect(_close_new_game_setup) _settings_button.pressed.connect(_open_settings) _credits_button.pressed.connect(_open_credits) _delete_button.pressed.connect(_on_delete_pressed) %QuitButton.pressed.connect(_on_quit_pressed) _confirmation_page.confirmed.connect(_on_confirmation_accepted) _confirmation_page.cancelled.connect(_request_close_confirmation) _settings_panel.applied.connect(_on_settings_applied) _settings_panel.closed.connect(_on_settings_closed) _settings_panel.closing.connect(_on_settings_closing) _settings_panel.opened.connect(_on_settings_opened) _credits_page.back_requested.connect(_close_credits) _save_slots_page.back_requested.connect(_close_save_slots) _save_slots_page.play_requested.connect(_on_slot_play_requested) _save_slots_page.create_requested.connect(_on_new_slot_requested) _bubble_field.configure(_get_title_buttons()) _bubble_field.motion_scale = 1.0 visibility_changed.connect(_on_title_visibility_changed) resized.connect(_update_responsive_title_stage) get_window().size_changed.connect(_update_responsive_title_stage) _start_prompt_label.resized.connect(_update_start_prompt_pivot) set_process(false) call_deferred("_update_responsive_title_stage") call_deferred("_update_start_prompt_pivot") call_deferred("_capture_title_bubble_rest_position") call_deferred("_capture_title_content_rest_position") func setup( save_manager: SaveManagerType, save_slots: PlayerSaveSlotCatalog, settings_manager: SettingsManagerType, network_session: NetworkSessionType, saved_servers: SavedServerStoreType, server_trust: ServerTrustStore, discovery: DiscoveryClient, data_root: PlayerDataRoot, interface_fonts: InterfaceFontController, ) -> void: _save_manager = save_manager _settings_manager = settings_manager _save_slots_page.setup(save_slots, data_root, interface_fonts) _join_game_page.setup( network_session, saved_servers, false, server_trust, discovery ) _join_game_page.join_requested.connect(join_game_requested.emit) _join_game_page.back_requested.connect(_close_join_game) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _refresh_save_inspection() show() _begin_title_entry() func reopen() -> void: _cancel_title_entry_transition() _cancel_title_settings_transition() Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _prepare_awaiting_start_input() _reset_confirmation() _settings_panel.hide() _join_game_page.close_page() _new_game_setup_page.close_page() _credits_page.close_page() _save_slots_page.close_page() _refresh_save_inspection() show() _start_entry_prompt_animation() func reopen_to_menu() -> void: reopen() _awaiting_start_input = false _stop_entry_prompt_animation() _start_prompt_center.hide() _presentation_center.show() _button_center.show() var bubble_field := get_node_or_null("%BubbleField") as Control if bubble_field != null: bubble_field.show() _set_title_bubbles_interactive(true) func refresh_after_data_root_change() -> void: _save_slots_page.refresh_page() _refresh_save_inspection() func open_join_game_page(endpoint: String = "") -> void: _cancel_title_entry_transition() _awaiting_start_input = false _set_title_bubbles_interactive(false) _release_primary_menu_focus() _start_prompt_center.hide() _presentation_center.hide() _settings_panel.hide() _confirmation_page.hide_page() _credits_page.close_page() _new_game_setup_page.close_page() _save_slots_page.close_page() _join_game_page.open_page(endpoint) func report_network_error(message: String) -> void: _join_game_page.set_status(message) if _save_slots_page.visible: _save_slots_page.set_status(message) return if not _join_game_page.visible: _feedback_label.text = _center_feedback_text(message) _feedback_label.show() _feedback_label.modulate.a = 1.0 func _open_join_game() -> void: if ( _action_in_progress or _is_confirmation_active() or _new_game_setup_page.visible or _credits_page.visible or _save_slots_page.visible ): return open_join_game_page() func _close_join_game() -> void: _join_game_page.close_page() _presentation_center.show() _button_center.show() _start_prompt_center.hide() _set_title_bubbles_interactive(true) _focus_initial_button() func _open_credits() -> void: if ( _action_in_progress or _is_confirmation_active() or _settings_panel.visible or _join_game_page.visible or _new_game_setup_page.visible or _credits_page.visible or _save_slots_page.visible ): return _hide_continue_stats_context() var restore_navigation_focus: bool = _navigation_focus_active _set_title_bubbles_interactive(false) _release_primary_menu_focus() _presentation_center.hide() _start_prompt_center.hide() _credits_page.open_page(restore_navigation_focus) func _close_credits(restore_navigation_focus: bool) -> void: _credits_page.close_page() _presentation_center.show() _button_center.show() _start_prompt_center.hide() _set_title_bubbles_interactive(true) if restore_navigation_focus: _navigation_focus_active = true _credits_button.grab_focus() else: _navigation_focus_active = false _release_title_focus() _update_continue_stats_visibility() func is_awaiting_start_input() -> bool: return _awaiting_start_input func _update_title_layout() -> void: if not is_node_ready(): return var layout_size: Vector2 = _title_presentation_scale_root.size var available_width: float = maxf( 1.0, layout_size.x - TITLE_HORIZONTAL_MARGIN ) var logo_width: float = minf( clampf( layout_size.x * LOGO_WIDTH_FACTOR, LOGO_MIN_WIDTH, LOGO_MAX_WIDTH ), available_width ) _title_logo.custom_minimum_size = Vector2( logo_width, logo_width / LOGO_ASPECT_RATIO ) var branding_separation: float = float( _main_content.get_theme_constant("separation") ) var branding_size := Vector2( logo_width, logo_width / LOGO_ASPECT_RATIO + branding_separation + _version_row.get_combined_minimum_size().y ) _branding_slot.custom_minimum_size = branding_size _branding_motion_root.size = branding_size var field_width: float = minf(BUBBLE_FIELD_MAX_WIDTH, available_width) var field_height: float = ( BUBBLE_FIELD_COMPACT_HEIGHT if layout_size.y < BUBBLE_COMPACT_HEIGHT_THRESHOLD else BUBBLE_FIELD_DESKTOP_HEIGHT ) if layout_size.y < BUBBLE_COMPACT_HEIGHT_THRESHOLD: field_width = minf(BUBBLE_FIELD_COMPACT_WIDTH, available_width) _bubble_layout_slot.custom_minimum_size = Vector2(field_width, field_height) _bubble_motion_root.size = Vector2(field_width, field_height) _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 ) _bubble_field.position = Vector2( 0.0, BUBBLE_FIELD_DESKTOP_OFFSET_Y * field_height / BUBBLE_FIELD_DESKTOP_HEIGHT, ) _configure_title_controller_navigation() if not _title_settings_transition_active: call_deferred("_capture_title_bubble_rest_position") if _awaiting_start_input and not _title_entry_transition_active: _schedule_intro_presentation() _update_world_preview_resolution() func _update_responsive_title_stage() -> void: if not is_node_ready(): return var display_size := Vector2(get_window().size) _title_presentation_scale_root.size = TITLE_DESKTOP_REFERENCE_SIZE _title_presentation_scale_root.scale = Vector2.ONE * ( FullscreenMenuPresentationType.get_profile_scale(display_size) ) _title_presentation_scale_root.position = ( FullscreenMenuPresentationType.get_profile_position(display_size) ) _update_title_layout() if ( _is_confirmation_active() and not _confirmation_page.is_transitioning() ): _set_confirmation_stage_rect() func _get_title_buttons() -> Array[BubbleButton]: return [ _play_button, _join_game_button, _settings_button, _credits_button, _quit_button, ] func set_world_pixelation(pixel_size: int) -> void: _world_pixel_size = clampi( pixel_size, PlayerSettings.MIN_WORLD_PIXEL_SIZE, PlayerSettings.MAX_WORLD_PIXEL_SIZE ) if is_node_ready(): _update_world_preview_resolution() func _update_world_preview_resolution() -> void: var displayed_size := Vector2i( maxi(1, roundi(size.x)), maxi(1, roundi(size.y)) ) var grid_size: Vector2i = PlayerSettings.get_world_grid_size( _world_pixel_size, displayed_size ) var logo_material := _title_logo.material as ShaderMaterial if logo_material != null: var render_scale: float = ( float(grid_size.y) / float(displayed_size.y) ) var effect_scale: float = 1.0 / render_scale logo_material.set_shader_parameter( "horizontal_displacement_pixels", minf(2.0, 1.5 * effect_scale) ) logo_material.set_shader_parameter( "bob_amount_pixels", minf(4.0, 3.0 * effect_scale) ) func _process(delta: float) -> void: if not visible: return 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 _main_content.visible and _button_center.visible: _bubble_field.advance_motion(delta) func _input(event: InputEvent) -> void: if not visible: return if ( _settings_panel.visible and _settings_panel.is_input_mapping_capturing() ): return if _credits_page.visible: if _credits_page.handle_input(event): get_viewport().set_input_as_handled() return if _join_game_page.visible: if event.is_action_pressed("ui_cancel"): _join_game_page.request_back() get_viewport().set_input_as_handled() return if _new_game_setup_page.visible: if event.is_action_pressed("ui_cancel"): _new_game_setup_page.request_back() get_viewport().set_input_as_handled() return if _save_slots_page.visible: if event.is_action_pressed("ui_cancel"): _save_slots_page.request_back() get_viewport().set_input_as_handled() return if ( _title_settings_transition_active or _title_entry_transition_active or _confirmation_transition_active ): get_viewport().set_input_as_handled() return if _awaiting_start_input: if not _is_start_prompt_reveal_event(event): return if not _intro_geometry_ready: get_viewport().set_input_as_handled() return _reveal_primary_menu() get_viewport().set_input_as_handled() return if _handle_primary_menu_focus_input(event): get_viewport().set_input_as_handled() return if not event.is_action_pressed("ui_cancel"): return if _is_confirmation_active(): _request_close_confirmation() elif _settings_panel.visible: _settings_panel.handle_back() else: return get_viewport().set_input_as_handled() func _is_start_prompt_reveal_event(event: InputEvent) -> bool: if event is InputEventKey: var key_event := event as InputEventKey return key_event.pressed and not key_event.echo if event is InputEventJoypadButton: return (event as InputEventJoypadButton).pressed if event is InputEventMouseButton: return (event as InputEventMouseButton).pressed return false func _handle_primary_menu_focus_input(event: InputEvent) -> bool: if ( not _button_center.visible or _is_confirmation_active() or _settings_panel.visible or _new_game_setup_page.visible or _credits_page.visible or _save_slots_page.visible ): return false if event is InputEventMouseMotion: _navigation_focus_active = false _release_primary_menu_focus() _update_continue_stats_visibility() return false if event is InputEventKey and (event as InputEventKey).echo: return false var moves_directionally: bool = ( event.is_action_pressed("ui_down") or event.is_action_pressed("ui_right") or event.is_action_pressed("ui_up") or event.is_action_pressed("ui_left") ) if not moves_directionally: return false _navigation_focus_active = true if _primary_menu_has_focus(): return false _get_first_available_menu_button().grab_focus() return true func _get_first_available_menu_button() -> Button: return _play_button func _primary_menu_has_focus() -> bool: var focus_owner: Control = get_viewport().gui_get_focus_owner() return focus_owner != null and _button_center.is_ancestor_of(focus_owner) func _release_primary_menu_focus() -> void: var focus_owner: Control = get_viewport().gui_get_focus_owner() if focus_owner != null and _button_center.is_ancestor_of(focus_owner): focus_owner.release_focus() func _release_title_focus() -> void: var focus_owner: Control = get_viewport().gui_get_focus_owner() if focus_owner != null and is_ancestor_of(focus_owner): focus_owner.release_focus() func _begin_title_entry() -> void: _prepare_awaiting_start_input() _start_entry_prompt_animation() func _prepare_awaiting_start_input() -> void: _cancel_title_entry_transition() _awaiting_start_input = true _intro_geometry_ready = false _navigation_focus_active = false _modal_restore_navigation_focus = false _button_center.show() var bubble_field := get_node_or_null("%BubbleField") as Control if bubble_field != null: bubble_field.hide() call_deferred("set_process", visible) _feedback_label.show() _feedback_label.modulate.a = 0.0 _continue_stats_hovered = false _continue_stats_focused = false _cancel_continue_stats_fade() _continue_stats_drawer.hide() _continue_stats_drawer.modulate.a = 0.0 _continue_stats_drawer.scale = Vector2(1.0, 0.001) _main_content.show() _start_prompt_center.show() _start_prompt_center.modulate.a = 1.0 _set_title_bubbles_interactive(false) var focus_owner: Control = get_viewport().gui_get_focus_owner() if focus_owner != null and is_ancestor_of(focus_owner): focus_owner.release_focus() _schedule_intro_presentation() func _start_entry_prompt_animation() -> void: _start_prompt_elapsed = 0.0 _start_prompt_label.scale = Vector2.ONE * START_PROMPT_MIN_SCALE _update_start_prompt_pivot() set_process(true) func _stop_entry_prompt_animation() -> void: set_process(visible and _button_center.visible) _start_prompt_elapsed = 0.0 _start_prompt_label.scale = Vector2.ONE func _update_start_prompt_pivot() -> void: if not is_node_ready(): return _start_prompt_label.pivot_offset = _start_prompt_label.size * 0.5 func _reveal_primary_menu() -> void: if ( not _awaiting_start_input or not _intro_geometry_ready or _title_entry_transition_active ): return _awaiting_start_input = false _button_center.show() var bubble_field := get_node_or_null("%BubbleField") as Control if bubble_field != null: bubble_field.show() _stop_entry_prompt_animation() _title_entry_generation += 1 _title_entry_transition_active = true _set_title_bubbles_interactive(false) var generation: int = _title_entry_generation set_process(true) _navigation_focus_active = false _release_title_focus() _bubble_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_QUAD).set_ease(Tween.EASE_IN) _title_entry_transition.tween_property( _branding_motion_root, "position", Vector2.ZERO, INTRO_LOGO_FLOAT_DURATION ).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) _title_entry_transition.tween_property( _bubble_field, "scale", Vector2.ONE, INTRO_MENU_REVEAL_DURATION ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT).set_delay( INTRO_MENU_REVEAL_DELAY ) _title_entry_transition.tween_property( _bubble_field, "modulate:a", 1.0, INTRO_MENU_REVEAL_DURATION ).set_delay(INTRO_MENU_REVEAL_DELAY) _title_entry_transition.finished.connect( _finish_primary_menu_reveal.bind(generation), CONNECT_ONE_SHOT ) func _finish_primary_menu_reveal(generation: int) -> void: if ( generation != _title_entry_generation or not _title_entry_transition_active ): return _title_entry_transition = null _title_entry_transition_active = false _start_prompt_center.hide() _start_prompt_center.modulate.a = 1.0 _branding_motion_root.position = Vector2.ZERO _bubble_field.scale = Vector2.ONE _bubble_field.modulate.a = 1.0 _feedback_label.modulate.a = 0.0 _set_title_bubbles_interactive(true) func _cancel_title_entry_transition() -> void: _title_entry_generation += 1 if _title_entry_transition != null: _title_entry_transition.kill() _title_entry_transition = null _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: _intro_layout_generation += 1 _intro_geometry_ready = false call_deferred( "_prepare_intro_presentation", _intro_layout_generation ) func _prepare_intro_presentation(generation: int) -> void: await get_tree().process_frame await get_tree().process_frame if ( generation != _intro_layout_generation or not is_node_ready() or not _awaiting_start_input or _title_entry_transition_active ): return _branding_motion_root.size = _branding_slot.size _bubble_motion_root.size = _bubble_layout_slot.size _bubble_motion_root.position = Vector2.ZERO _title_content_rest_position = _presentation_center.position _title_bubble_rest_position = _bubble_field.position var branding_rect: Rect2 = _get_title_stage_rect(_branding_slot) var desired_branding_center_y: float = ( _title_presentation_scale_root.size.y * INTRO_LOGO_CENTER_Y_RATIO ) _branding_motion_root.position = Vector2( 0.0, desired_branding_center_y - branding_rect.get_center().y, ) _intro_geometry_ready = true func _get_title_stage_rect(control: Control) -> Rect2: var inverse_stage_transform: Transform2D = ( _title_presentation_scale_root.get_global_transform().affine_inverse() ) var global_rect: Rect2 = control.get_global_rect() var local_position: Vector2 = ( inverse_stage_transform * global_rect.position ) var local_end: Vector2 = inverse_stage_transform * global_rect.end return Rect2(local_position, local_end - local_position) func _on_continue_stats_hover_changed(hovered: bool) -> void: _continue_stats_hovered = hovered _update_continue_stats_visibility() func _on_continue_stats_focus_changed(focused: bool) -> void: _continue_stats_focused = focused _update_continue_stats_visibility() func _update_continue_stats_visibility() -> void: if not is_node_ready(): return var requested_visible: bool = ( _continue_stats_hovered or ( _continue_stats_focused and _navigation_focus_active ) ) requested_visible = ( requested_visible and not _play_button.disabled and _inspection != null and _inspection.status == SaveInspectionType.Status.VALID_SUPPORTED and not _awaiting_start_input and not _title_entry_transition_active and not _title_settings_transition_active and not _settings_panel.visible and not _is_confirmation_active() and not _action_in_progress and _presentation_center.visible ) if requested_visible: _continue_stats_drawer.text = _get_continue_stats_text() _fade_continue_stats_to(1.0 if requested_visible else 0.0) func _hide_continue_stats_context() -> void: _continue_stats_hovered = false _continue_stats_focused = false _fade_continue_stats_to(0.0) func _fade_continue_stats_to(target_opacity: float) -> void: if not is_node_ready(): return _cancel_continue_stats_fade() var target_scale := Vector2( 1.0, 1.0 if target_opacity > 0.0 else 0.001, ) if target_opacity > 0.0: _continue_stats_drawer.show() if ( is_equal_approx( _continue_stats_drawer.modulate.a, target_opacity, ) and _continue_stats_drawer.scale.is_equal_approx(target_scale) ): _continue_stats_drawer.modulate.a = target_opacity _continue_stats_drawer.scale = target_scale if target_opacity <= 0.0: _continue_stats_drawer.hide() return _continue_stats_fade = create_tween().set_parallel(true) _continue_stats_fade.tween_property( _continue_stats_drawer, "modulate:a", target_opacity, QUICK_MENU_ENTER_DURATION ).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) _continue_stats_fade.tween_property( _continue_stats_drawer, "scale", target_scale, QUICK_MENU_ENTER_DURATION ).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) if target_opacity <= 0.0: _continue_stats_fade.chain().tween_callback( _finish_continue_stats_hide ) func _finish_continue_stats_hide() -> void: _continue_stats_fade = null if _continue_stats_drawer.modulate.a <= 0.001: _continue_stats_drawer.hide() func _cancel_continue_stats_fade() -> void: if _continue_stats_fade != null: _continue_stats_fade.kill() _continue_stats_fade = null func _get_continue_stats_text() -> String: if ( _inspection == null or _inspection.status != SaveInspectionType.Status.VALID_SUPPORTED ): return "" return _center_feedback_text( "%d fish • %s • %d discovered" % [ _inspection.catch_count, CurrencyPresentationType.bbcode_amount( _inspection.wallet_balance, 18 ), _inspection.discovered_species_count, ] ) func _center_feedback_text(message: String) -> String: if message.begins_with("[center]"): return message return "[center]%s[/center]" % message func _on_continue_pressed() -> void: if ( _action_in_progress or _is_confirmation_active() or _settings_panel.visible or _save_slots_page.visible ): return _hide_continue_stats_context() _modal_restore_navigation_focus = _navigation_focus_active _set_title_bubbles_interactive(false) _release_primary_menu_focus() _presentation_center.hide() _start_prompt_center.hide() _join_game_page.close_page() _credits_page.close_page() _new_game_setup_page.close_page() _save_slots_page.open_page() func _close_save_slots() -> void: _save_slots_page.close_page() _presentation_center.show() _button_center.show() _start_prompt_center.hide() _set_title_bubbles_interactive(true) var restore_navigation_focus: bool = _modal_restore_navigation_focus _modal_restore_navigation_focus = false if restore_navigation_focus: _navigation_focus_active = true _play_button.grab_focus() else: _navigation_focus_active = false _release_title_focus() _update_continue_stats_visibility() func _on_slot_play_requested(slot_id: String) -> void: if _action_in_progress or not _save_slots_page.visible: return _action_in_progress = true slot_play_requested.emit(slot_id) _action_in_progress = false if visible: _save_slots_page.finish_request_if_pending( "the save slot could not be started." ) func _on_new_slot_requested( display_name: String, world_layout: StringName, world_seed: int, duplicate_source_slot_id: String, ) -> void: if _action_in_progress or not _save_slots_page.visible: return _action_in_progress = true new_slot_requested.emit( display_name, world_layout, world_seed, duplicate_source_slot_id, ) _action_in_progress = false if visible: _save_slots_page.finish_request_if_pending( "the save slot could not be created." ) func _on_new_game_pressed() -> void: if ( _action_in_progress or _is_confirmation_active() or _settings_panel.visible or _inspection == null ): return _hide_continue_stats_context() if _inspection.status == SaveInspectionType.Status.UNSUPPORTED_VERSION: _feedback_label.text = _center_feedback_text( "this save is from a newer game version. " + "use delete save before starting over." ) return if _inspection.status == SaveInspectionType.Status.IO_ERROR: _feedback_label.text = _center_feedback_text( "the existing save cannot be accessed safely." ) return _open_new_game_setup( _inspection.status != SaveInspectionType.Status.MISSING ) func _open_new_game_setup(has_existing_progression: bool) -> void: _modal_restore_navigation_focus = _navigation_focus_active _set_title_bubbles_interactive(false) _release_primary_menu_focus() _presentation_center.hide() _start_prompt_center.hide() _join_game_page.close_page() _credits_page.close_page() _new_game_setup_page.open_page(has_existing_progression) func _close_new_game_setup() -> void: _new_game_setup_page.close_page() _presentation_center.show() _button_center.show() _start_prompt_center.hide() _set_title_bubbles_interactive(true) var restore_navigation_focus: bool = _modal_restore_navigation_focus _modal_restore_navigation_focus = false if restore_navigation_focus: _navigation_focus_active = true _new_game_button.grab_focus() else: _navigation_focus_active = false _release_title_focus() _update_continue_stats_visibility() func _start_configured_new_game( world_layout: StringName, world_seed: int, ) -> void: if _action_in_progress or not _new_game_setup_page.visible: return _new_game_setup_page.close_page() _action_in_progress = true new_game_requested.emit(world_layout, world_seed) _action_in_progress = false # Signal delivery is synchronous. A successful start hides this title screen; # if it remains visible, restore the menu so a generation error is recoverable. if visible: _presentation_center.show() _button_center.show() _set_title_bubbles_interactive(true) _navigation_focus_active = true _new_game_button.grab_focus() func _on_delete_pressed() -> void: if ( _action_in_progress or _is_confirmation_active() or _settings_panel.visible or _inspection == null or not _inspection.can_delete() ): return _hide_continue_stats_context() _open_confirmation( ConfirmationAction.DELETE_SAVE, "delete your saved progression? this cannot be undone.", "delete" ) func _on_confirmation_accepted() -> void: if ( _action_in_progress or _confirmation_transition_active or _confirmation_action == ConfirmationAction.NONE ): return var action: ConfirmationAction = _confirmation_action _confirmation_page.lock_interaction() _action_in_progress = true if not _save_manager.delete_progression_save(): _feedback_label.text = _center_feedback_text( "failed to delete saved progression." ) _action_in_progress = false _refresh_save_inspection() _begin_confirmation_return() return _save_manager.initialize_new_game() _refresh_save_inspection() _feedback_label.text = _center_feedback_text("saved progression deleted.") _begin_confirmation_return() _action_in_progress = false func _open_confirmation( action: ConfirmationAction, message: String, confirm_text: String, use_multiline_action_layout: bool = false, ) -> void: _hide_continue_stats_context() _modal_restore_navigation_focus = _navigation_focus_active _confirmation_action = action _confirmation_page.configure( message, confirm_text, use_multiline_action_layout ) _begin_confirmation_open() func _request_close_confirmation() -> void: if ( not _confirmation_page.visible or _confirmation_transition_active or _action_in_progress ): return _begin_confirmation_return() func _begin_confirmation_open() -> void: _confirmation_transition_generation += 1 _cancel_confirmation_transition() _confirmation_transition_active = true _confirmation_title_content_rest_position = _presentation_center.position _set_confirmation_stage_rect() _confirmation_page.hide_page() _set_title_bubbles_interactive(false) _release_primary_menu_focus() var generation: int = _confirmation_transition_generation _presentation_center.pivot_offset = _presentation_center.size * 0.5 _confirmation_transition = create_tween().set_parallel(true) _confirmation_transition.tween_property( _presentation_center, "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 ) func _finish_confirmation_title_exit(generation: int) -> void: if ( generation != _confirmation_transition_generation or not _confirmation_transition_active ): return _confirmation_transition = null _presentation_center.hide() _presentation_center.position = _confirmation_title_content_rest_position _reset_quick_menu_presentation(_presentation_center) _confirmation_page.transition_in( QUICK_MENU_ENTER_DURATION, _finish_confirmation_open.bind(generation) ) func _finish_confirmation_open(generation: int) -> void: if ( generation != _confirmation_transition_generation or not _confirmation_transition_active ): return _confirmation_transition_active = false func _begin_confirmation_return() -> void: if not _confirmation_page.visible or _confirmation_transition_active: return _confirmation_transition_generation += 1 _cancel_confirmation_transition() _confirmation_transition_active = true _confirmation_page.lock_interaction() var generation: int = _confirmation_transition_generation _confirmation_page.transition_out( QUICK_MENU_EXIT_DURATION, _finish_confirmation_page_exit.bind(generation) ) func _finish_confirmation_page_exit(generation: int) -> void: if ( generation != _confirmation_transition_generation or not _confirmation_transition_active ): return _confirmation_action = ConfirmationAction.NONE _presentation_center.position = _confirmation_title_content_rest_position _presentation_center.show() _prepare_quick_menu_enter(_presentation_center) _confirmation_transition = create_tween().set_parallel(true) _confirmation_transition.tween_property( _presentation_center, "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 ) func _finish_confirmation_return(generation: int) -> void: if ( generation != _confirmation_transition_generation or not _confirmation_transition_active ): return _confirmation_transition = null _confirmation_transition_active = false _presentation_center.position = _confirmation_title_content_rest_position _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 if restore_navigation_focus: _focus_initial_button() else: _navigation_focus_active = false _release_title_focus() _update_continue_stats_visibility() func _set_confirmation_stage_rect() -> void: var slot_rect: Rect2 = _get_title_stage_rect(_bubble_layout_slot) _confirmation_page.set_stage_rect( slot_rect ) func _reset_confirmation() -> void: _confirmation_transition_generation += 1 _cancel_confirmation_transition() _confirmation_transition_active = false _confirmation_action = ConfirmationAction.NONE _confirmation_page.hide_page() if is_node_ready(): _presentation_center.position = _confirmation_title_content_rest_position _reset_quick_menu_presentation(_presentation_center) _presentation_center.show() func _cancel_confirmation_transition() -> void: if _confirmation_transition != null: _confirmation_transition.kill() _confirmation_transition = null func _is_confirmation_active() -> bool: return ( _confirmation_action != ConfirmationAction.NONE or _confirmation_page.visible or _confirmation_transition_active ) func _open_settings() -> void: if ( _is_confirmation_active() or _action_in_progress or _settings_panel.visible or _credits_page.visible or _save_slots_page.visible or _title_settings_transition_active ): return _hide_continue_stats_context() _modal_restore_navigation_focus = _navigation_focus_active _feedback_visible_before_settings = _feedback_label.visible _feedback_text_before_settings = _feedback_label.text _begin_title_cluster_exit() func _close_settings() -> void: _settings_panel.close_panel() func _on_settings_applied() -> void: _feedback_text_before_settings = "settings saved." func _on_settings_closed() -> void: _refresh_save_inspection() func _on_settings_closing() -> void: await get_tree().create_timer( UIMotion.BUBBLE_TRANSITION_OVERLAP_DELAY ).timeout if _settings_panel.visible: _begin_title_cluster_return(_feedback_text_before_settings) func _on_settings_opened() -> void: pass func _begin_title_cluster_exit() -> void: _title_settings_transition_generation += 1 _cancel_title_settings_tween() _title_settings_transition_active = true _title_content_rest_position = _presentation_center.position _title_bubble_rest_position = _bubble_field.position _set_title_bubbles_interactive(false) _release_primary_menu_focus() var generation: int = _title_settings_transition_generation _presentation_center.pivot_offset = _presentation_center.size * 0.5 _title_settings_transition = create_tween().set_parallel(true) _title_settings_transition.tween_property( _presentation_center, "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 ) await get_tree().create_timer( UIMotion.BUBBLE_TRANSITION_OVERLAP_DELAY ).timeout if ( generation == _title_settings_transition_generation and _title_settings_transition_active ): _settings_panel.open_panel( _settings_manager, SettingsPanelType.PresentationMode.TITLE_EMBEDDED ) func _finish_title_cluster_exit(generation: int) -> void: if ( generation != _title_settings_transition_generation or not _title_settings_transition_active ): return _title_settings_transition = null _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: _title_settings_transition_generation += 1 _cancel_title_settings_tween() _title_settings_transition_active = true var centered_feedback: String = _center_feedback_text(feedback_text) if _feedback_label.text != centered_feedback: _feedback_label.text = centered_feedback _feedback_label.visible = _feedback_visible_before_settings _hide_continue_stats_context() _set_title_bubbles_interactive(false) var generation: int = _title_settings_transition_generation _presentation_center.position = _title_content_rest_position _presentation_center.show() _prepare_quick_menu_enter(_presentation_center) _title_settings_transition = create_tween().set_parallel(true) _title_settings_transition.tween_property( _presentation_center, "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 ) func _finish_title_cluster_return(generation: int) -> void: if ( generation != _title_settings_transition_generation or not _title_settings_transition_active ): return _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() _update_continue_stats_visibility() func _set_title_bubbles_interactive(interactive: bool) -> void: for bubble: BubbleButton in _get_title_buttons(): if not interactive: bubble.reset_hover_presentation() bubble.focus_mode = ( Control.FOCUS_ALL if interactive else Control.FOCUS_NONE ) bubble.mouse_filter = ( Control.MOUSE_FILTER_STOP if interactive else Control.MOUSE_FILTER_IGNORE ) if interactive: _configure_title_controller_navigation() func _configure_title_controller_navigation() -> void: ControllerFocusNavigationType.configure_spatial_neighbors( _get_title_buttons() ) _play_button.focus_neighbor_right = ( _play_button.get_path_to(_join_game_button) ) _join_game_button.focus_neighbor_left = ( _join_game_button.get_path_to(_play_button) ) _join_game_button.focus_neighbor_bottom = ( _join_game_button.get_path_to(_settings_button) ) _settings_button.focus_neighbor_left = ( _settings_button.get_path_to(_play_button) ) _settings_button.focus_neighbor_top = ( _settings_button.get_path_to(_join_game_button) ) _settings_button.focus_neighbor_bottom = ( _settings_button.get_path_to(_credits_button) ) _credits_button.focus_neighbor_left = ( _credits_button.get_path_to(_play_button) ) _credits_button.focus_neighbor_top = ( _credits_button.get_path_to(_settings_button) ) _credits_button.focus_neighbor_bottom = ( _credits_button.get_path_to(_quit_button) ) _quit_button.focus_neighbor_left = ( _quit_button.get_path_to(_play_button) ) _quit_button.focus_neighbor_top = ( _quit_button.get_path_to(_credits_button) ) func _capture_title_bubble_rest_position() -> void: if ( not is_node_ready() or _title_settings_transition_active or not _button_center.visible ): return _title_bubble_rest_position = _bubble_field.position func _capture_title_content_rest_position() -> void: if ( not is_node_ready() or _title_settings_transition_active or _title_entry_transition_active or not _button_center.visible ): return _title_content_rest_position = _presentation_center.position func _cancel_title_settings_transition() -> void: _title_settings_transition_generation += 1 _cancel_title_settings_tween() _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) func _cancel_title_settings_tween() -> void: if _title_settings_transition != null: _title_settings_transition.kill() _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 if restore_navigation_focus: _navigation_focus_active = true _settings_button.grab_focus() else: _navigation_focus_active = false _release_title_focus() func _refresh_save_inspection() -> void: _inspection = _save_manager.inspect_save() _play_button.disabled = false _delete_button.disabled = not _inspection.can_delete() _feedback_label.text = _center_feedback_text(_inspection.message) if _inspection.status == SaveInspectionType.Status.VALID_SUPPORTED: _continue_stats_drawer.text = _get_continue_stats_text() if not _inspection.can_continue(): _hide_continue_stats_context() else: _update_continue_stats_visibility() func _focus_initial_button() -> void: if ( not is_node_ready() or not visible or _awaiting_start_input or not _button_center.visible or _credits_page.visible or _save_slots_page.visible ): return _play_button.grab_focus() _navigation_focus_active = true _update_continue_stats_visibility() func _on_quit_pressed() -> void: if ( not _action_in_progress and not _is_confirmation_active() and not _settings_panel.visible and not _credits_page.visible ): _hide_continue_stats_context() quit_requested.emit() func _on_title_visibility_changed() -> void: if visible: set_process(true) else: _hide_continue_stats_context() _stop_entry_prompt_animation() _navigation_focus_active = false _modal_restore_navigation_focus = false _reset_confirmation() _credits_page.close_page()