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:
parent
1db1a5b754
commit
3b84bfe3a0
97 changed files with 7869 additions and 982 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue