feat: expand progression and multiplayer systems

Add named save slots, progression import/export, and a unified play flow. Add live friend requests, presence, invitations, and relationship controls without durable discovery-server social storage. Advance the network protocol with isolated channels, movement reconciliation, late-join recovery, fishing replication, and animation synchronization. Preserve per-species catch totals, refine generated-world startup and water recovery, and complete the related input and interface improvements.
This commit is contained in:
Alexander Sellite 2026-08-23 20:48:38 -04:00
parent 1db1a5b754
commit 3b84bfe3a0
97 changed files with 7869 additions and 982 deletions

View file

@ -27,6 +27,7 @@ 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"
)
@ -89,6 +90,13 @@ const QUICK_MENU_ENTER_SCALE: float = 0.97
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)
@ -97,7 +105,7 @@ enum ConfirmationAction {
DELETE_SAVE,
}
@onready var _continue_button: BubbleButtonType = %ContinueButton
@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
@ -134,6 +142,7 @@ enum ConfirmationAction {
@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
@ -183,17 +192,17 @@ func _ready() -> void:
).strip_edges()
if not release_version.is_empty():
_playtest_label.text = "v%s" % release_version
_continue_button.pressed.connect(_on_continue_pressed)
_continue_button.mouse_entered.connect(
_play_button.pressed.connect(_on_continue_pressed)
_play_button.mouse_entered.connect(
_on_continue_stats_hover_changed.bind(true)
)
_continue_button.mouse_exited.connect(
_play_button.mouse_exited.connect(
_on_continue_stats_hover_changed.bind(false)
)
_continue_button.focus_entered.connect(
_play_button.focus_entered.connect(
_on_continue_stats_focus_changed.bind(true)
)
_continue_button.focus_exited.connect(
_play_button.focus_exited.connect(
_on_continue_stats_focus_changed.bind(false)
)
_new_game_button.pressed.connect(_on_new_game_pressed)
@ -214,6 +223,9 @@ func _ready() -> void:
_emit_navigation_bubble_flurry
)
_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 = 0.0
_decorative_fish_timer.timeout.connect(_on_decorative_fish_timer_timeout)
@ -237,14 +249,18 @@ func _ready() -> void:
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
)
@ -276,6 +292,7 @@ func reopen() -> void:
_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_decorative_presentation()
@ -304,11 +321,15 @@ func open_join_game_page(endpoint: String = "") -> void:
_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()
@ -321,6 +342,7 @@ func _open_join_game() -> void:
or _is_confirmation_active()
or _new_game_setup_page.visible
or _credits_page.visible
or _save_slots_page.visible
):
return
open_join_game_page()
@ -342,6 +364,7 @@ func _open_credits() -> void:
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()
@ -469,12 +492,10 @@ func _update_responsive_title_stage() -> void:
func _get_title_buttons() -> Array[BubbleButton]:
return [
_continue_button,
_new_game_button,
_play_button,
_join_game_button,
_settings_button,
_credits_button,
_delete_button,
_quit_button,
]
@ -583,6 +604,11 @@ func _input(event: InputEvent) -> void:
_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
@ -631,6 +657,7 @@ func _handle_primary_menu_focus_input(event: InputEvent) -> bool:
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:
@ -656,7 +683,7 @@ func _handle_primary_menu_focus_input(event: InputEvent) -> bool:
func _get_first_available_menu_button() -> Button:
return _continue_button if not _continue_button.disabled else _new_game_button
return _play_button
func _primary_menu_has_focus() -> bool:
@ -868,7 +895,7 @@ func _update_continue_stats_visibility() -> void:
)
requested_visible = (
requested_visible
and not _continue_button.disabled
and not _play_button.disabled
and _inspection != null
and _inspection.status == SaveInspectionType.Status.VALID_SUPPORTED
and not _awaiting_start_input
@ -940,14 +967,70 @@ func _on_continue_pressed() -> void:
_action_in_progress
or _is_confirmation_active()
or _settings_panel.visible
or _inspection == null
or not _inspection.can_continue()
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
continue_game_requested.emit()
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:
@ -1252,6 +1335,7 @@ func _open_settings() -> void:
or _action_in_progress
or _settings_panel.visible
or _credits_page.visible
or _save_slots_page.visible
or _title_settings_transition_active
):
return
@ -1467,12 +1551,12 @@ func _restore_settings_focus() -> void:
func _refresh_save_inspection() -> void:
_inspection = _save_manager.inspect_save()
_continue_button.disabled = not _inspection.can_continue()
_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:
_feedback_label.text = _get_continue_stats_text()
if _continue_button.disabled:
if not _inspection.can_continue():
_hide_continue_stats_context()
else:
_update_continue_stats_visibility()
@ -1485,12 +1569,10 @@ func _focus_initial_button() -> void:
or _awaiting_start_input
or not _button_center.visible
or _credits_page.visible
or _save_slots_page.visible
):
return
if not _continue_button.disabled:
_continue_button.grab_focus()
else:
_new_game_button.grab_focus()
_play_button.grab_focus()
_navigation_focus_active = true
_update_continue_stats_visibility()