1587 lines
47 KiB
GDScript
1587 lines
47 KiB
GDScript
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()
|