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
169
ui/chat_ui.gd
169
ui/chat_ui.gd
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue