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

@ -28,6 +28,7 @@ const SPEECH_POINTER_OVERLAP: float = 3.0
const ANIMALESE_FULL_VOLUME_DISTANCE: float = 4.0
const ANIMALESE_SILENT_DISTANCE: float = 24.0
const ANIMALESE_SILENT_VOLUME_DB: float = -80.0
const MAX_EDITOR_GIVE_BALANCE: int = 1_000_000_000_000
const MOBILE_COMPACT_WIDTH: float = 620.0
const MOBILE_EXPANDED_WIDTH: float = 820.0
const MOBILE_COMPACT_HEIGHT: float = 220.0
@ -142,6 +143,14 @@ var _send_pending: bool = false
var _pending_send_body: String = ""
var _controller_refocused: bool = false
var _input_lock_applied: bool = false
var _fishing_input_priority_active: bool = false
var _fishing_resume_pending: bool = false
var _suspended_chat_state_valid: bool = false
var _suspended_chat_text: String = ""
var _suspended_chat_caret: int = 0
var _suspended_chat_had_selection: bool = false
var _suspended_chat_selection_from: int = 0
var _suspended_chat_selection_to: int = 0
var _last_submit_frame: int = -1
var _output_scale: float = 1.0
var _dock_right: bool = false
@ -325,6 +334,7 @@ func _update_status_effect_icons() -> void:
func open_chat() -> void:
if (
_opened or not _available or _service == null
or _fishing_input_priority_active
or not _session.is_gameplay_session_active()
):
return
@ -361,12 +371,115 @@ func open_command_chat() -> void:
func set_available(value: bool) -> void:
_available = value
if not value:
_clear_suspended_chat_state()
_send_pending = false
_pending_send_body = ""
_entry.editable = true
close_chat()
_flush_draft()
_refresh_visibility()
_flush_draft()
_refresh_visibility()
elif _fishing_resume_pending and not _fishing_input_priority_active:
call_deferred("_resume_chat_after_fishing")
func set_fishing_input_priority(active: bool) -> void:
if _fishing_input_priority_active == active:
return
_fishing_input_priority_active = active
if active:
if not _opened:
return
_capture_suspended_chat_state()
_fishing_resume_pending = not _send_pending
close_chat(true)
elif _fishing_resume_pending:
call_deferred("_resume_chat_after_fishing")
func has_fishing_resume_pending() -> bool:
return _fishing_resume_pending
func get_text_entry_control() -> LineEdit:
return _entry
func _capture_suspended_chat_state() -> void:
_suspended_chat_state_valid = true
_suspended_chat_text = _entry.text
_suspended_chat_caret = _entry.caret_column
_suspended_chat_had_selection = _entry.has_selection()
if _suspended_chat_had_selection:
_suspended_chat_selection_from = (
_entry.get_selection_from_column()
)
_suspended_chat_selection_to = _entry.get_selection_to_column()
else:
_suspended_chat_selection_from = 0
_suspended_chat_selection_to = 0
func _resume_chat_after_fishing() -> void:
if _fishing_input_priority_active or not _fishing_resume_pending:
return
if (
not _suspended_chat_state_valid
or not _available
or _service == null
or _session == null
or not _session.is_gameplay_session_active()
):
_clear_suspended_chat_state()
return
var restored_text: String = _suspended_chat_text
var restored_caret: int = _suspended_chat_caret
var restored_had_selection: bool = _suspended_chat_had_selection
var restored_selection_from: int = _suspended_chat_selection_from
var restored_selection_to: int = _suspended_chat_selection_to
_clear_suspended_chat_state()
_entry.text = restored_text
_entry.caret_column = clampi(restored_caret, 0, restored_text.length())
open_chat()
if not _opened:
return
call_deferred(
"_restore_suspended_chat_edit_state",
restored_caret,
restored_had_selection,
restored_selection_from,
restored_selection_to,
)
func _restore_suspended_chat_edit_state(
caret: int,
had_selection: bool,
selection_from: int,
selection_to: int,
) -> void:
if not _opened or not _entry.visible:
return
var text_length: int = _entry.text.length()
_entry.caret_column = clampi(caret, 0, text_length)
if had_selection:
_entry.select(
clampi(selection_from, 0, text_length),
clampi(selection_to, 0, text_length),
)
else:
_entry.deselect()
_entry.grab_focus()
_refresh_input_ownership()
func _clear_suspended_chat_state() -> void:
_fishing_resume_pending = false
_suspended_chat_state_valid = false
_suspended_chat_text = ""
_suspended_chat_caret = 0
_suspended_chat_had_selection = false
_suspended_chat_selection_from = 0
_suspended_chat_selection_to = 0
func close_chat(preserve_status: bool = false) -> void:
@ -942,7 +1055,7 @@ func _send() -> void:
if submitted_text.length() == 0:
close_chat()
return
if _handle_editor_world_command(submitted_text):
if _handle_editor_command(submitted_text):
return
_send_pending = true
_pending_send_body = submitted_text
@ -959,29 +1072,27 @@ func _send() -> void:
_set_status("Sending…")
func _handle_editor_world_command(body: String) -> bool:
func _handle_editor_command(body: String) -> bool:
# These commands are deliberately limited to sessions launched by the
# Godot editor. Exported builds do not have the editor feature tag, and a
# joined editor client must not be able to mutate its host's world.
# Godot editor. Exported builds do not have the editor feature tag.
if not OS.has_feature("editor"):
return false
var command_text: String = body.strip_edges()
if not (
command_text.begins_with("/time")
or command_text.begins_with("/weather")
):
return false
var parts: PackedStringArray = command_text.split(" ", false)
if parts.is_empty() or not String(parts[0]).begins_with("/"):
return false
var command: String = String(parts[0]).trim_prefix("/").to_lower()
if command not in ["give", "time", "weather"]:
return false
var result: String = ""
if _session == null or not _session.is_host():
if command == "give":
result = _apply_editor_give_command(parts)
elif _session == null or not _session.is_host():
result = "Editor world commands require the authoritative host."
elif command == "time":
result = _apply_editor_time_command(parts)
elif command == "weather":
result = _apply_editor_weather_command(parts)
else:
return false
result = _apply_editor_weather_command(parts)
_entry.clear()
_flush_draft()
_set_status(result)
@ -989,6 +1100,28 @@ func _handle_editor_world_command(body: String) -> bool:
return true
func _apply_editor_give_command(parts: PackedStringArray) -> String:
if parts.size() != 2 or _player == null or _player.wallet == null:
return "Usage: /give [positive integer]"
var amount_text: String = String(parts[1])
if not amount_text.is_valid_int():
return "Usage: /give [positive integer]"
var amount: int = amount_text.to_int()
var current_balance: int = _player.wallet.get_balance()
if amount <= 0:
return "Usage: /give [positive integer]"
if current_balance > MAX_EDITOR_GIVE_BALANCE - amount:
return "Editor balance cannot exceed %d fish coins." % (
MAX_EDITOR_GIVE_BALANCE
)
if not _player.wallet.credit(amount):
return "Editor fish coins could not be added."
return "Added %d fish coins. Balance: %d." % [
amount,
_player.wallet.get_balance(),
]
func _apply_editor_time_command(parts: PackedStringArray) -> String:
if parts.size() != 2 or _world_time == null:
return "Usage: /time [dawn, day, dusk, night]"
@ -1043,6 +1176,7 @@ func _on_local_message_confirmed(message: Dictionary) -> void:
return
_send_pending = false
_pending_send_body = ""
_clear_suspended_chat_state()
_entry.editable = true
_entry.clear()
_set_status("")
@ -1194,6 +1328,11 @@ func _on_rejected(message: String) -> void:
_pending_send_body = ""
_entry.editable = true
_set_status(message)
if _suspended_chat_state_valid:
_fishing_resume_pending = true
if not _fishing_input_priority_active:
call_deferred("_resume_chat_after_fishing")
return
if not _opened:
open_chat()
else: