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

@ -6,6 +6,7 @@ const JoinGamePageScene = preload(
const TitleScreenScene = preload("res://ui/title_screen.tscn")
const PauseMenuScene = preload("res://ui/pause_menu.tscn")
const SettingsPanelScene = preload("res://ui/settings_panel.tscn")
const SaveSlotsPageScene = preload("res://ui/save_slots_page.tscn")
const BubbleConfirmationScene = preload(
"res://ui/components/bubble_menu/bubble_confirmation_page.tscn"
)
@ -43,6 +44,7 @@ func _initialize() -> void:
func _run() -> void:
root.size = Vector2i(1280, 720)
await _validate_primary_menu_navigation()
await _validate_save_slots_navigation()
await _validate_join_game_navigation()
await _validate_data_settings_navigation()
await _validate_settings_adjustment_navigation()
@ -113,8 +115,20 @@ func _validate_primary_menu_navigation() -> void:
(control as BaseButton).disabled = false
title_controls.append(control)
_expect(
title_controls.size() == 7,
"Title menu does not expose all seven primary actions.",
title_controls.size() == 5,
"Title menu does not expose its five primary actions.",
)
var play_button := title.get_node("%PlayButton") as Button
_expect(
play_button.icon != null
and play_button.icon.resource_path
== "res://ui/icons/main_menu/continue.png",
"The Play action does not use the original Continue icon.",
)
_expect(
not (title.get_node("%NewGameButton") as Control).visible
and not (title.get_node("%DeleteSaveButton") as Control).visible,
"Legacy New/Delete title bubbles are still visible.",
)
_assert_directionally_reachable(title_controls.front(), title_controls)
title.queue_free()
@ -141,12 +155,67 @@ func _validate_primary_menu_navigation() -> void:
await process_frame
func _validate_save_slots_navigation() -> void:
var page := SaveSlotsPageScene.instantiate() as SaveSlotsPage
root.add_child(page)
await process_frame
page.open_page()
for _frame: int in 2:
await process_frame
await create_timer(0.25).timeout
var saves_tab := page.get_node("%SavesTab") as Button
var new_tab := page.get_node("%NewSlotTab") as Button
var content_panel := page.get_node("%ContentPanel") as PanelContainer
var tab_overlap: float = (
saves_tab.get_global_rect().end.y
- content_panel.get_global_rect().position.y
)
_expect(
is_equal_approx(tab_overlap, saves_tab.size.y * 0.5),
"Save-slot content does not cover the lower half of its organizer tabs.",
)
_expect(
page.get_active_page_id() == &"new",
"An empty save catalog does not open directly to New Slot.",
)
_assert_neighbor(saves_tab, &"focus_neighbor_right", new_tab)
_assert_neighbor(new_tab, &"focus_neighbor_left", saves_tab)
var new_controls: Array[Control] = [
saves_tab,
new_tab,
page.get_node("%NewSlotName") as Control,
page.get_node("%GeneratedButton") as Control,
page.get_node("%StarterButton") as Control,
page.get_node("%RandomSeedButton") as Control,
page.get_node("%CustomSeedButton") as Control,
page.get_node("%CreateSlotButton") as Control,
page.get_node("%BackButton") as Control,
]
_assert_directionally_reachable(new_tab, new_controls)
page.call("_select_page", &"saves", false)
await process_frame
var import_button := page.get_node("%ImportSlotButton") as Button
_expect(
saves_tab.find_valid_focus_neighbor(SIDE_BOTTOM) == import_button,
"An empty Save Slots page does not lead from its tab to Import Save.",
)
_expect(
import_button.find_valid_focus_neighbor(SIDE_BOTTOM)
== page.get_node("%BackButton"),
"An empty Save Slots page does not lead from Import Save to Back.",
)
page.queue_free()
await process_frame
func _validate_join_game_navigation() -> void:
var page := JoinGamePageScene.instantiate() as Control
root.add_child(page)
await process_frame
page.show()
await process_frame
var discover := page.get_node("%DiscoverButton") as Button
var friends := page.get_node("%FriendsButton") as Button
var direct := page.get_node("%DirectButton") as Button
var saved := page.get_node("%SavedButton") as Button
var recent := page.get_node("%RecentButton") as Button
@ -161,8 +230,21 @@ func _validate_join_game_navigation() -> void:
var delete := page.get_node("%DeleteButton") as Button
var cancel := page.get_node("%CancelButton") as Button
var back := page.get_node("%BackButton") as Button
var modes: Array[Control] = [discover, direct, saved, recent]
var direct_content := page.get_node("%DirectContent") as Control
var list_content := page.get_node("%ListContent") as Control
var content_panel := page.get_node("%ContentPanel") as PanelContainer
var modes: Array[Control] = [discover, friends, direct, saved, recent]
var tab_overlap: float = (
discover.get_global_rect().end.y
- content_panel.get_global_rect().position.y
)
_expect(
is_equal_approx(tab_overlap, discover.size.y * 0.5),
"Join-game content does not cover the lower half of its organizer tabs.",
)
direct_content.hide()
list_content.show()
address.hide()
name_edit.hide()
server_list.show()
@ -174,14 +256,15 @@ func _validate_join_game_navigation() -> void:
_set_button_state(delete, false)
_set_button_state(cancel, false)
_set_button_state(back, true)
page.set("_mode", 0)
page.set("_mode", JoinGamePage.Mode.DISCOVER)
page.call("_configure_controller_navigation")
await process_frame
_assert_neighbor(discover, &"focus_neighbor_bottom", server_list)
_assert_neighbor(server_list, &"focus_neighbor_top", discover)
_assert_neighbor(server_list, &"focus_neighbor_bottom", refresh)
_assert_neighbor(refresh, &"focus_neighbor_right", join)
_assert_neighbor(back, &"focus_neighbor_left", join)
_assert_neighbor(join, &"focus_neighbor_bottom", back)
_assert_neighbor(back, &"focus_neighbor_top", join)
var discover_controls: Array[Control] = modes.duplicate()
discover_controls.append_array([server_list, refresh, join, back])
_assert_directionally_reachable(discover, discover_controls)
@ -209,18 +292,31 @@ func _validate_join_game_navigation() -> void:
"Discovery's visible cursor and selected room should agree immediately.",
)
page.set("_mode", JoinGamePage.Mode.FRIENDS)
page.call("_configure_controller_navigation")
await process_frame
_assert_neighbor(friends, &"focus_neighbor_bottom", server_list)
_assert_neighbor(server_list, &"focus_neighbor_top", friends)
var friend_controls: Array[Control] = modes.duplicate()
friend_controls.append_array([server_list, refresh, join, back])
_assert_directionally_reachable(friends, friend_controls)
list_content.hide()
direct_content.show()
address.show()
address.editable = true
server_list.hide()
_set_button_state(refresh, false)
_set_button_state(join, true)
_set_button_state(save, true)
page.set("_mode", 1)
page.set("_mode", JoinGamePage.Mode.DIRECT)
page.call("_configure_controller_navigation")
await process_frame
_assert_neighbor(direct, &"focus_neighbor_bottom", address)
_assert_neighbor(address, &"focus_neighbor_top", direct)
_assert_neighbor(address, &"focus_neighbor_bottom", join)
_assert_neighbor(save, &"focus_neighbor_bottom", back)
_assert_neighbor(back, &"focus_neighbor_top", save)
var direct_controls: Array[Control] = modes.duplicate()
direct_controls.append_array([address, join, save, back])
_assert_directionally_reachable(direct, direct_controls)
@ -251,8 +347,10 @@ func _validate_join_game_navigation() -> void:
entry.display_name = "Saved server %d" % index
saved_entries.append(entry)
server_list.add_item(entry.display_name)
page.set("_mode", 2)
page.set("_mode", JoinGamePage.Mode.SAVED)
page.set("_visible_entries", saved_entries)
direct_content.hide()
list_content.show()
server_list.show()
page.call("_configure_controller_navigation")
page.call("_restore_entry_selection_and_focus", 1)
@ -387,8 +485,6 @@ func _validate_data_settings_navigation() -> void:
"Open Data Folder left a controller-activatable dialog behind.",
)
var change_data_folder := panel.get_node("%ChangeDataFolder") as Button
var export_progression := panel.get_node("%ExportProgression") as Button
var import_progression := panel.get_node("%ImportProgression") as Button
var copy_fingerprint := panel.get_node("%CopyPlayerFingerprint") as Button
var export_player := panel.get_node("%ExportPlayerIdentity") as Button
var import_player := panel.get_node("%ImportPlayerIdentity") as Button
@ -398,8 +494,6 @@ func _validate_data_settings_navigation() -> void:
data_tab,
open_data_folder,
change_data_folder,
export_progression,
import_progression,
copy_fingerprint,
export_player,
import_player,
@ -423,7 +517,7 @@ func _validate_data_settings_navigation() -> void:
_assert_neighbor(
open_data_folder,
&"focus_neighbor_bottom",
export_progression,
copy_fingerprint,
)
_assert_neighbor(
change_data_folder,
@ -433,43 +527,13 @@ func _validate_data_settings_navigation() -> void:
_assert_neighbor(
change_data_folder,
&"focus_neighbor_bottom",
import_progression,
copy_fingerprint,
)
_assert_neighbor(
export_progression,
copy_fingerprint,
&"focus_neighbor_top",
open_data_folder,
)
_assert_neighbor(
export_progression,
&"focus_neighbor_right",
import_progression,
)
_assert_neighbor(
export_progression,
&"focus_neighbor_bottom",
copy_fingerprint,
)
_assert_neighbor(
import_progression,
&"focus_neighbor_top",
change_data_folder,
)
_assert_neighbor(
import_progression,
&"focus_neighbor_left",
export_progression,
)
_assert_neighbor(
import_progression,
&"focus_neighbor_bottom",
copy_fingerprint,
)
_assert_neighbor(
copy_fingerprint,
&"focus_neighbor_top",
export_progression,
)
_assert_neighbor(
copy_fingerprint,
&"focus_neighbor_bottom",
@ -582,6 +646,8 @@ func _validate_settings_adjustment_navigation() -> void:
var on_screen_keyboard := panel.get_node(
"%OnScreenKeyboardToggle"
) as Button
var swap_scroll := panel.get_node("%SwapScrollToggle") as Button
var invert_y := panel.get_node("%InvertYToggle") as Button
var controller_binds := panel.get_node("%ControllerMapping") as Button
var keyboard_binds := panel.get_node("%KeyboardMapping") as Button
_expect(
@ -594,6 +660,26 @@ func _validate_settings_adjustment_navigation() -> void:
and is_equal_approx(controller_slider.step, 0.1),
"Sensitivity sliders do not retain their authored increments.",
)
_assert_neighbor(
invert_y,
&"focus_neighbor_bottom",
swap_scroll,
)
_assert_neighbor(
swap_scroll,
&"focus_neighbor_top",
invert_y,
)
_assert_neighbor(
swap_scroll,
&"focus_neighbor_bottom",
on_screen_keyboard,
)
_assert_neighbor(
on_screen_keyboard,
&"focus_neighbor_top",
swap_scroll,
)
_assert_neighbor(
on_screen_keyboard,
&"focus_neighbor_bottom",
@ -623,7 +709,8 @@ func _validate_settings_adjustment_navigation() -> void:
panel.get_node("%ControlsTab") as Control,
mouse_slider,
controller_slider,
panel.get_node("%InvertYToggle") as Control,
invert_y,
swap_scroll,
on_screen_keyboard,
controller_binds,
keyboard_binds,