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:
|
||||
|
|
|
|||
191
ui/game_ui.gd
191
ui/game_ui.gd
|
|
@ -79,6 +79,8 @@ signal passive_pointer_ui_changed(is_enabled: bool)
|
|||
signal player_menu_backdrop_visibility_changed(is_visible: bool)
|
||||
signal shop_backdrop_visibility_changed(is_visible: bool)
|
||||
signal virtual_pointer_mode_changed(is_active: bool)
|
||||
signal social_prompt_accepted
|
||||
signal social_prompt_declined
|
||||
|
||||
const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse"
|
||||
const EMOTE_RADIAL_CAMERA_OWNER: StringName = &"emote_radial_menu"
|
||||
|
|
@ -139,10 +141,12 @@ const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
|
|||
@onready var _screen_fade: ScreenFade = %ScreenFade
|
||||
@onready var _title_screen: TitleScreenType = %TitleScreen
|
||||
@onready var _pause_menu: PauseMenuType = %PauseMenu
|
||||
@onready var _social_prompt: BubbleConfirmationPage = %SocialPrompt
|
||||
@onready var _hotbar_ui: HotbarUIType = %Hotbar
|
||||
@onready var _fishing_shop: FishingShopType = %FishingShop
|
||||
@onready var _player_storage: PlayerStorageType = %PlayerStorage
|
||||
@onready var _storage_prompt: PanelContainer = %StoragePrompt
|
||||
@onready var _storage_prompt_message: Label = %StoragePromptMessage
|
||||
@onready var _shop_prompt: Control = %ShopPrompt
|
||||
@onready var _shop_prompt_bubble: PanelContainer = %ShopPromptBubble
|
||||
@onready var _shop_prompt_message: Label = %ShopPromptMessage
|
||||
|
|
@ -182,6 +186,8 @@ var _gameplay_ui_enabled: bool = false
|
|||
var _gameplay_hud_hidden: bool = false
|
||||
var _fishing_spot: FishingSpotType
|
||||
var _system_menu_open: bool = false
|
||||
var _social_prompt_open: bool = false
|
||||
var _social_prompt_restore_system_menu: bool = false
|
||||
var _shop_open: bool = false
|
||||
var _storage_open: bool = false
|
||||
var _chat_input_open: bool = false
|
||||
|
|
@ -208,16 +214,22 @@ var _virtual_mouse_stick: Vector2 = Vector2.ZERO
|
|||
var _virtual_mouse_trigger_rest_by_device: Dictionary[int, float] = {}
|
||||
var _shared_trigger_rest_by_device: Dictionary[int, float] = {}
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _keyboard_mouse_mapping_manager: KeyboardMouseMappingManagerType
|
||||
var _settings_manager: PlayerSettingsManagerType
|
||||
var _controller_text_entry_request: Callable
|
||||
var _controller_text_entry_is_open: Callable
|
||||
var _controller_text_entry_close: Callable
|
||||
var _restore_chat_keyboard_after_fishing: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_prioritize_surface_drawing_pointer_input()
|
||||
_bite_prompt_button.pressed.connect(_on_bite_prompt_pressed)
|
||||
_social_prompt.confirmed.connect(_accept_social_prompt)
|
||||
_social_prompt.cancelled.connect(_decline_social_prompt)
|
||||
_apply_active_bait_indicator_style()
|
||||
_refresh_active_bait_indicator()
|
||||
_refresh_interaction_prompt_bindings()
|
||||
# Reward feedback must remain above full-screen canonical menus. Keeping the
|
||||
# overlay as the final stage child makes that ownership explicit instead of
|
||||
# relying on scene declaration order when another menu adds high-z children.
|
||||
|
|
@ -268,9 +280,11 @@ func _ready() -> void:
|
|||
func set_controller_text_entry_request(
|
||||
request: Callable,
|
||||
is_open: Callable = Callable(),
|
||||
close: Callable = Callable(),
|
||||
) -> void:
|
||||
_controller_text_entry_request = request
|
||||
_controller_text_entry_is_open = is_open
|
||||
_controller_text_entry_close = close
|
||||
|
||||
|
||||
func request_controller_text_entry_for(control: Control = null) -> bool:
|
||||
|
|
@ -278,7 +292,15 @@ func request_controller_text_entry_for(control: Control = null) -> bool:
|
|||
if target == null:
|
||||
target = get_viewport().gui_get_focus_owner()
|
||||
return (
|
||||
bool(_controller_text_entry_request.call(target))
|
||||
bool(_controller_text_entry_request.call(target, false))
|
||||
if _controller_text_entry_request.is_valid()
|
||||
else false
|
||||
)
|
||||
|
||||
|
||||
func _resume_controller_text_entry_for(control: Control) -> bool:
|
||||
return (
|
||||
bool(_controller_text_entry_request.call(control, true))
|
||||
if _controller_text_entry_request.is_valid()
|
||||
else false
|
||||
)
|
||||
|
|
@ -292,6 +314,14 @@ func is_controller_text_entry_open() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func _close_controller_text_entry_for(control: Control) -> bool:
|
||||
return (
|
||||
bool(_controller_text_entry_close.call(control))
|
||||
if _controller_text_entry_close.is_valid()
|
||||
else false
|
||||
)
|
||||
|
||||
|
||||
func setup(
|
||||
player: PlayerType,
|
||||
inventory: FishInventoryType,
|
||||
|
|
@ -386,6 +416,9 @@ func setup(
|
|||
)
|
||||
fishing_spot.status_changed.connect(_on_fishing_status_changed)
|
||||
fishing_spot.bite_prompt_changed.connect(_on_bite_prompt_changed)
|
||||
fishing_spot.fishing_input_priority_changed.connect(
|
||||
_on_fishing_input_priority_changed
|
||||
)
|
||||
fishing_spot.catch_display_changed.connect(_on_catch_display_changed)
|
||||
fishing_spot.showcase_changed.connect(_on_showcase_changed)
|
||||
_player_menu.menu_visibility_changed.connect(
|
||||
|
|
@ -480,6 +513,15 @@ func setup(
|
|||
_shop_interaction = shop_interaction
|
||||
_storage_interaction = storage_interaction
|
||||
_surface_drawing = surface_drawing
|
||||
if (
|
||||
_surface_drawing != null
|
||||
and not _surface_drawing.hud_state_changed.is_connected(
|
||||
_on_surface_drawing_hud_state_changed
|
||||
)
|
||||
):
|
||||
_surface_drawing.hud_state_changed.connect(
|
||||
_on_surface_drawing_hud_state_changed
|
||||
)
|
||||
_surface_drawing_toolbar.setup(_surface_drawing, art_unlocks)
|
||||
set_edge_docks(
|
||||
settings_manager.current_settings.chat_dock_right,
|
||||
|
|
@ -520,6 +562,10 @@ func _prioritize_surface_drawing_pointer_input() -> void:
|
|||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if _social_prompt_open and event.is_action_pressed("ui_cancel"):
|
||||
_decline_social_prompt()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
# The on-screen keyboard owns controller input while it is open. Its
|
||||
# overlay is processed before the UI beneath it and consumes the event.
|
||||
if is_controller_text_entry_open():
|
||||
|
|
@ -1336,7 +1382,6 @@ func _drawing_pointer_window_position() -> Vector2:
|
|||
|
||||
func setup_data_and_identity(
|
||||
data_root: PlayerDataRoot,
|
||||
progression_saves: PlayerSaveManager,
|
||||
identity_backups: IdentityBackupService,
|
||||
player_identity: PlayerIdentityStore,
|
||||
host_identity: HostIdentityStore,
|
||||
|
|
@ -1348,7 +1393,6 @@ func setup_data_and_identity(
|
|||
]:
|
||||
panel.setup_data_and_identity(
|
||||
data_root,
|
||||
progression_saves,
|
||||
identity_backups,
|
||||
player_identity,
|
||||
host_identity,
|
||||
|
|
@ -1375,10 +1419,64 @@ func setup_controller_mapping(
|
|||
func setup_keyboard_mouse_mapping(
|
||||
mapping_manager: KeyboardMouseMappingManagerType,
|
||||
) -> void:
|
||||
if (
|
||||
_keyboard_mouse_mapping_manager != null
|
||||
and _keyboard_mouse_mapping_manager.mapping_changed.is_connected(
|
||||
_refresh_interaction_prompt_bindings
|
||||
)
|
||||
):
|
||||
_keyboard_mouse_mapping_manager.mapping_changed.disconnect(
|
||||
_refresh_interaction_prompt_bindings
|
||||
)
|
||||
_keyboard_mouse_mapping_manager = mapping_manager
|
||||
if (
|
||||
_keyboard_mouse_mapping_manager != null
|
||||
and not _keyboard_mouse_mapping_manager.mapping_changed.is_connected(
|
||||
_refresh_interaction_prompt_bindings
|
||||
)
|
||||
):
|
||||
_keyboard_mouse_mapping_manager.mapping_changed.connect(
|
||||
_refresh_interaction_prompt_bindings
|
||||
)
|
||||
for panel: SettingsPanelType in [
|
||||
_title_settings_panel, _pause_settings_panel
|
||||
]:
|
||||
panel.setup_keyboard_mouse_mapping(mapping_manager)
|
||||
_refresh_interaction_prompt_bindings()
|
||||
|
||||
|
||||
func _refresh_interaction_prompt_bindings() -> void:
|
||||
if not is_node_ready():
|
||||
return
|
||||
var interact_label: String = "interact"
|
||||
if _keyboard_mouse_mapping_manager != null:
|
||||
var resolved_label: String = (
|
||||
_keyboard_mouse_mapping_manager.get_binding_label(
|
||||
KeyboardMouseMappingManagerType.ROLE_INTERACT
|
||||
)
|
||||
)
|
||||
if resolved_label not in ["", "unmapped", "unknown key"]:
|
||||
interact_label = resolved_label
|
||||
if interact_label.length() == 1:
|
||||
interact_label = interact_label.to_upper()
|
||||
_shop_prompt_key.text = interact_label
|
||||
_storage_prompt_message.text = "%s open storage" % interact_label
|
||||
_resize_interaction_prompts()
|
||||
|
||||
|
||||
func _resize_interaction_prompts() -> void:
|
||||
var badge_width: float = maxf(
|
||||
24.0,
|
||||
ceilf(_shop_prompt_key.get_combined_minimum_size().x) + 12.0,
|
||||
)
|
||||
_shop_prompt_key_badge.position.x = _shop_prompt.size.x - badge_width + 6.0
|
||||
_shop_prompt_key_badge.size.x = badge_width
|
||||
var storage_width: float = maxf(
|
||||
220.0,
|
||||
ceilf(_storage_prompt_message.get_combined_minimum_size().x) + 24.0,
|
||||
)
|
||||
_storage_prompt.custom_minimum_size.x = storage_width
|
||||
_storage_prompt.size.x = storage_width
|
||||
|
||||
|
||||
func is_controller_mapping_capturing() -> bool:
|
||||
|
|
@ -1709,6 +1807,52 @@ func set_system_menu_open(is_open: bool) -> void:
|
|||
_emit_interactive_pointer_ui_changed()
|
||||
|
||||
|
||||
func show_social_prompt(
|
||||
message: String,
|
||||
accept_text: String = "accept",
|
||||
decline_text: String = "decline",
|
||||
) -> bool:
|
||||
if _social_prompt_open:
|
||||
return false
|
||||
_social_prompt_open = true
|
||||
_social_prompt_restore_system_menu = _system_menu_open
|
||||
set_system_menu_open(true)
|
||||
_social_prompt.configure(
|
||||
message,
|
||||
accept_text,
|
||||
decline_text,
|
||||
BubbleConfirmationPage.InitialFocus.CONFIRM,
|
||||
)
|
||||
_social_prompt.transition_in(0.08, func() -> void: pass)
|
||||
return true
|
||||
|
||||
|
||||
func is_social_prompt_open() -> bool:
|
||||
return _social_prompt_open
|
||||
|
||||
|
||||
func _accept_social_prompt() -> void:
|
||||
_resolve_social_prompt(true)
|
||||
|
||||
|
||||
func _decline_social_prompt() -> void:
|
||||
_resolve_social_prompt(false)
|
||||
|
||||
|
||||
func _resolve_social_prompt(accepted: bool) -> void:
|
||||
if not _social_prompt_open or _social_prompt.is_transitioning():
|
||||
return
|
||||
_social_prompt.lock_interaction()
|
||||
_social_prompt.transition_out(0.08, func() -> void:
|
||||
_social_prompt_open = false
|
||||
set_system_menu_open(_social_prompt_restore_system_menu)
|
||||
if accepted:
|
||||
social_prompt_accepted.emit()
|
||||
else:
|
||||
social_prompt_declined.emit()
|
||||
)
|
||||
|
||||
|
||||
func get_fishing_shop() -> FishingShopType:
|
||||
return _fishing_shop
|
||||
|
||||
|
|
@ -1930,6 +2074,9 @@ func _on_player_settings_changed(settings: PlayerSettings) -> void:
|
|||
_player_menu.set_profile_preview_world_pixel_size(
|
||||
settings.world_pixel_size
|
||||
)
|
||||
_hotbar_ui.set_swap_hotbar_camera_scroll(
|
||||
settings.swap_hotbar_camera_scroll
|
||||
)
|
||||
|
||||
|
||||
func set_edge_docks(
|
||||
|
|
@ -1979,6 +2126,18 @@ func _on_fishing_status_changed(status: String) -> void:
|
|||
_set_fishing_status(status)
|
||||
|
||||
|
||||
func _on_surface_drawing_hud_state_changed(
|
||||
_is_active: bool,
|
||||
_mode_name: String,
|
||||
_color_name: String,
|
||||
_color_value: Color,
|
||||
_brush_size: int,
|
||||
_grid_size: int,
|
||||
status: String,
|
||||
) -> void:
|
||||
_set_fishing_status(status)
|
||||
|
||||
|
||||
func _set_fishing_status(text: String) -> void:
|
||||
var normalized_text: String = text.strip_edges()
|
||||
_status_label.text = normalized_text
|
||||
|
|
@ -2004,6 +2163,31 @@ func _on_bite_prompt_changed(prompt_visible: bool) -> void:
|
|||
_refresh_fishing_panel_visibility()
|
||||
|
||||
|
||||
func _on_fishing_input_priority_changed(active: bool) -> void:
|
||||
if active:
|
||||
_restore_chat_keyboard_after_fishing = (
|
||||
_chat_ui.is_open() and is_controller_text_entry_open()
|
||||
)
|
||||
if _restore_chat_keyboard_after_fishing:
|
||||
_close_controller_text_entry_for(
|
||||
_chat_ui.get_text_entry_control()
|
||||
)
|
||||
_chat_ui.set_fishing_input_priority(active)
|
||||
if not active and _restore_chat_keyboard_after_fishing:
|
||||
call_deferred("_restore_chat_keyboard_after_fishing_ends")
|
||||
|
||||
|
||||
func _restore_chat_keyboard_after_fishing_ends() -> void:
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if not _restore_chat_keyboard_after_fishing:
|
||||
return
|
||||
_restore_chat_keyboard_after_fishing = false
|
||||
if not _chat_ui.is_open():
|
||||
return
|
||||
_resume_controller_text_entry_for(_chat_ui.get_text_entry_control())
|
||||
|
||||
|
||||
func _on_bite_prompt_pressed() -> void:
|
||||
if _fishing_spot != null:
|
||||
_fishing_spot.confirm_pending_bite()
|
||||
|
|
@ -2023,7 +2207,6 @@ func _on_catch_display_changed(
|
|||
and _fishing_spot != null
|
||||
and _fishing_spot.is_fighting()
|
||||
)
|
||||
_hotbar_ui.set_item_name_suppressed(encounter_visible)
|
||||
_green_catch_progress.value = progress * 100.0
|
||||
_red_chase_progress.value = maxf(chase_progress, 0.0) * 100.0
|
||||
_catch_track.visible = encounter_visible
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=21 format=3]
|
||||
[gd_scene load_steps=22 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
|
||||
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
[ext_resource type="PackedScene" path="res://ui/quick_radial_menu.tscn" id="12_quick"]
|
||||
[ext_resource type="Script" path="res://ui/controller_virtual_cursor.gd" id="13_cursor"]
|
||||
[ext_resource type="PackedScene" path="res://ui/player_storage.tscn" id="14_storage"]
|
||||
[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_confirmation_page.tscn" id="15_social_prompt"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
|
||||
bg_color = Color(0.032, 0.118, 0.15, 1)
|
||||
|
|
@ -461,7 +462,6 @@ grow_horizontal = 2
|
|||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "E"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
|
|
@ -491,9 +491,10 @@ theme_override_constants/margin_top = 7
|
|||
theme_override_constants/margin_right = 12
|
||||
theme_override_constants/margin_bottom = 7
|
||||
|
||||
[node name="Message" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/StoragePrompt/Margin"]
|
||||
[node name="StoragePromptMessage" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/StoragePrompt/Margin"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "E open storage"
|
||||
text = "open storage"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
theme_override_font_sizes/font_size = 18
|
||||
|
|
@ -533,3 +534,8 @@ unique_name_in_owner = true
|
|||
|
||||
[node name="PauseMenu" parent="UIRoot/CanonicalStage" instance=ExtResource("6_pause")]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[node name="SocialPrompt" parent="UIRoot/CanonicalStage" instance=ExtResource("15_social_prompt")]
|
||||
unique_name_in_owner = true
|
||||
z_index = 3000
|
||||
z_as_relative = false
|
||||
|
|
|
|||
109
ui/hotbar.gd
109
ui/hotbar.gd
|
|
@ -9,7 +9,6 @@ const PlayerBagType = preload("res://inventory/player_bag.gd")
|
|||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||
const BubbleHotbarSlotType = preload(
|
||||
"res://ui/components/bubble_hotbar/bubble_hotbar_slot.gd"
|
||||
)
|
||||
|
|
@ -28,8 +27,6 @@ const HOTBAR_MENU_Z_INDEX: int = 90
|
|||
|
||||
@onready var _presentation_scale_root: Control = %HotbarPresentationScaleRoot
|
||||
@onready var _bubble_field: Control = %BubbleField
|
||||
@onready var _selected_item_label: Label = %SelectedItemLabel
|
||||
@onready var _item_name_timer: Timer = %ItemNameTimer
|
||||
|
||||
var _hotbar: PlayerHotbarType
|
||||
var _bag: PlayerBagType
|
||||
|
|
@ -39,8 +36,7 @@ var _fishing_spot: FishingSpotType
|
|||
var _slots: Array[BubbleHotbarSlotType] = []
|
||||
var _gameplay_input_enabled: bool = false
|
||||
var _drag_enabled: bool = false
|
||||
var _hovered_slot_index: int = -1
|
||||
var _item_name_suppressed: bool = false
|
||||
var _swap_hotbar_camera_scroll: bool = false
|
||||
var _motion_elapsed: float = 0.0
|
||||
var _compact_layout: bool = false
|
||||
var _player_menu_context: bool = false
|
||||
|
|
@ -56,7 +52,6 @@ var _visibility_generation: int = 0
|
|||
|
||||
|
||||
func _ready() -> void:
|
||||
_item_name_timer.timeout.connect(_on_item_name_timer_timeout)
|
||||
resized.connect(_apply_layout)
|
||||
_collect_slots()
|
||||
_apply_layout()
|
||||
|
|
@ -100,13 +95,14 @@ func set_gameplay_input_enabled(enabled: bool) -> void:
|
|||
_gameplay_input_enabled = enabled
|
||||
|
||||
|
||||
func set_swap_hotbar_camera_scroll(enabled: bool) -> void:
|
||||
_swap_hotbar_camera_scroll = enabled
|
||||
|
||||
|
||||
func set_drag_enabled(enabled: bool) -> void:
|
||||
_drag_enabled = enabled
|
||||
for slot: BubbleHotbarSlotType in _slots:
|
||||
slot.set_drag_enabled(enabled)
|
||||
if not enabled:
|
||||
_hovered_slot_index = -1
|
||||
_hide_item_name()
|
||||
|
||||
|
||||
func begin_controller_placement(
|
||||
|
|
@ -148,7 +144,6 @@ func end_controller_placement() -> void:
|
|||
for slot: BubbleHotbarSlotType in _slots:
|
||||
slot.focus_mode = Control.FOCUS_NONE
|
||||
slot.set_controller_placement_preview(false, null)
|
||||
_show_selected_item_briefly()
|
||||
|
||||
|
||||
func begin_controller_management(initial_slot: int) -> void:
|
||||
|
|
@ -178,7 +173,6 @@ func end_controller_management() -> void:
|
|||
_controller_management_active = false
|
||||
for slot: BubbleHotbarSlotType in _slots:
|
||||
slot.focus_mode = Control.FOCUS_NONE
|
||||
_show_selected_item_briefly()
|
||||
|
||||
|
||||
func _resolve_controller_placement_texture() -> Texture2D:
|
||||
|
|
@ -289,13 +283,6 @@ func set_presentation_visible(
|
|||
)
|
||||
|
||||
|
||||
func set_item_name_suppressed(suppressed: bool) -> void:
|
||||
_item_name_suppressed = suppressed
|
||||
if suppressed:
|
||||
_hovered_slot_index = -1
|
||||
_hide_item_name()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if (
|
||||
not _gameplay_input_enabled
|
||||
|
|
@ -321,7 +308,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
if (
|
||||
event is InputEventMouseButton
|
||||
and event.pressed
|
||||
and not event.shift_pressed
|
||||
and event.shift_pressed == _swap_hotbar_camera_scroll
|
||||
):
|
||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||
_hotbar.cycle_selection(-1)
|
||||
|
|
@ -337,10 +324,6 @@ func _collect_slots() -> void:
|
|||
var slot := child as BubbleHotbarSlotType
|
||||
if slot == null:
|
||||
continue
|
||||
slot.item_hovered.connect(_on_slot_item_hovered)
|
||||
slot.item_hover_ended.connect(_on_slot_item_hover_ended)
|
||||
slot.item_drag_started.connect(_on_slot_drag_started)
|
||||
slot.item_drag_finished.connect(_on_slot_drag_finished)
|
||||
slot.focus_entered.connect(
|
||||
_on_controller_slot_focused.bind(slot.slot_index)
|
||||
)
|
||||
|
|
@ -405,9 +388,6 @@ func _on_selected_slot_changed(
|
|||
_refresh()
|
||||
if _controller_placement_active:
|
||||
_refresh_controller_placement_preview()
|
||||
return
|
||||
if _hovered_slot_index < 0:
|
||||
_show_selected_item_briefly()
|
||||
|
||||
|
||||
func _on_controller_slot_focused(slot_index: int) -> void:
|
||||
|
|
@ -419,80 +399,3 @@ func _on_controller_slot_focused(slot_index: int) -> void:
|
|||
_hotbar.select_slot(slot_index)
|
||||
if _controller_placement_active:
|
||||
_refresh_controller_placement_preview()
|
||||
|
||||
|
||||
func _on_slot_item_hovered(
|
||||
slot_index: int,
|
||||
item_id: StringName,
|
||||
) -> void:
|
||||
if _item_name_suppressed:
|
||||
return
|
||||
_hovered_slot_index = slot_index
|
||||
_item_name_timer.stop()
|
||||
_show_assignment_name(slot_index, item_id)
|
||||
|
||||
|
||||
func _on_slot_item_hover_ended(slot_index: int) -> void:
|
||||
if slot_index != _hovered_slot_index:
|
||||
return
|
||||
_hovered_slot_index = -1
|
||||
_show_selected_item_briefly()
|
||||
|
||||
|
||||
func _on_slot_drag_started() -> void:
|
||||
_hovered_slot_index = -1
|
||||
_hide_item_name()
|
||||
|
||||
|
||||
func _on_slot_drag_finished() -> void:
|
||||
_show_selected_item_briefly()
|
||||
|
||||
|
||||
func _show_selected_item_briefly() -> void:
|
||||
if _item_name_suppressed or _hotbar == null:
|
||||
_hide_item_name()
|
||||
return
|
||||
var selected_slot: int = _hotbar.get_selected_slot()
|
||||
var identity: StringName = _hotbar.get_selected_item_id()
|
||||
if identity.is_empty():
|
||||
identity = _hotbar.get_selected_fish_catch_id()
|
||||
_show_assignment_name(selected_slot, identity)
|
||||
if _selected_item_label.visible:
|
||||
_item_name_timer.start()
|
||||
|
||||
|
||||
func _show_assignment_name(slot_index: int, identity: StringName) -> void:
|
||||
if identity.is_empty() or _hotbar == null:
|
||||
_hide_item_name()
|
||||
return
|
||||
var catch_id: StringName = _hotbar.get_fish_catch_id(slot_index)
|
||||
if not catch_id.is_empty() and _fish_inventory != null:
|
||||
var fish_catch: FishCatchType = _fish_inventory.get_catch_by_id(catch_id)
|
||||
if fish_catch != null:
|
||||
_selected_item_label.text = FishQualityType.qualified_name(
|
||||
fish_catch.fish.display_name,
|
||||
fish_catch.quality,
|
||||
)
|
||||
_selected_item_label.visible = true
|
||||
return
|
||||
var item = (
|
||||
_catalog.get_item_by_id(identity)
|
||||
if _catalog != null
|
||||
else null
|
||||
)
|
||||
if item == null:
|
||||
_hide_item_name()
|
||||
return
|
||||
_selected_item_label.text = item.display_name
|
||||
_selected_item_label.visible = true
|
||||
|
||||
|
||||
func _hide_item_name() -> void:
|
||||
_item_name_timer.stop()
|
||||
_selected_item_label.text = ""
|
||||
_selected_item_label.visible = false
|
||||
|
||||
|
||||
func _on_item_name_timer_timeout() -> void:
|
||||
if _hovered_slot_index < 0:
|
||||
_hide_item_name()
|
||||
|
|
|
|||
|
|
@ -105,33 +105,3 @@ slot_index = 8
|
|||
desktop_anchor = Vector2(735, 49)
|
||||
compact_anchor = Vector2(530, 42)
|
||||
motion_phase = 5.51
|
||||
|
||||
[node name="SelectedItemLabel" type="Label" parent="ResponsiveHotbarStage/HotbarPresentationScaleRoot"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 12
|
||||
anchor_left = 0.5
|
||||
anchor_top = 1.0
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -150.0
|
||||
offset_top = -132.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = -112.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
mouse_filter = 2
|
||||
clip_text = true
|
||||
text_overrun_behavior = 3
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
theme_override_colors/font_color = Color(0.925, 0.953, 0.965, 1)
|
||||
theme_override_colors/font_outline_color = Color(0.015, 0.02, 0.03, 0.95)
|
||||
theme_override_constants/outline_size = 2
|
||||
theme_override_font_sizes/font_size = 12
|
||||
|
||||
[node name="ItemNameTimer" type="Timer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
wait_time = 1.5
|
||||
one_shot = true
|
||||
|
|
|
|||
BIN
ui/icons/player_options/clean.png
Normal file
BIN
ui/icons/player_options/clean.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
40
ui/icons/player_options/clean.png.import
Normal file
40
ui/icons/player_options/clean.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bmhhqwmx7lxxy"
|
||||
path="res://.godot/imported/clean.png-762368d1ca0665fa46111797ef3210a1.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://ui/icons/player_options/clean.png"
|
||||
dest_files=["res://.godot/imported/clean.png-762368d1ca0665fa46111797ef3210a1.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=64
|
||||
detect_3d/compress_to=1
|
||||
BIN
ui/icons/player_options/friends.png
Normal file
BIN
ui/icons/player_options/friends.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
40
ui/icons/player_options/friends.png.import
Normal file
40
ui/icons/player_options/friends.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://brdj550pntc0t"
|
||||
path="res://.godot/imported/friends.png-2546aa51ed36f2defc5121467d12527d.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://ui/icons/player_options/friends.png"
|
||||
dest_files=["res://.godot/imported/friends.png-2546aa51ed36f2defc5121467d12527d.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=64
|
||||
detect_3d/compress_to=1
|
||||
|
|
@ -962,7 +962,11 @@ func _build_known_details(fish: FishDataType) -> void:
|
|||
fish.get_maximum_weight(),
|
||||
],
|
||||
)
|
||||
_add_detail_row(left_stats, "number caught", "unknown")
|
||||
_add_detail_row(
|
||||
left_stats,
|
||||
"number caught",
|
||||
str(_collection_log.get_catch_count(fish.id)),
|
||||
)
|
||||
_add_detail_row(right_stats, "rarity", fish.get_rarity_name().to_lower())
|
||||
_add_detail_row(right_stats, "time of day", _availability_text(fish))
|
||||
_add_detail_row(right_stats, "seasons", fish.get_season_text())
|
||||
|
|
@ -1127,6 +1131,7 @@ func _stats_overlay_text(fish: FishDataType, catalog_number: int) -> String:
|
|||
fish.get_minimum_weight(),
|
||||
fish.get_maximum_weight(),
|
||||
],
|
||||
"number caught: %d" % _collection_log.get_catch_count(fish.id),
|
||||
"rarity: %s" % fish.get_rarity_name().to_lower(),
|
||||
"time of day: %s" % _availability_text(fish),
|
||||
"seasons: %s" % fish.get_season_text(),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ signal back_requested
|
|||
|
||||
enum Mode {
|
||||
DISCOVER,
|
||||
FRIENDS,
|
||||
DIRECT,
|
||||
SAVED,
|
||||
RECENT,
|
||||
|
|
@ -26,6 +27,12 @@ const DIRECT_WORKFLOW_HELP: String = (
|
|||
% ADDRESS_FORMAT_HELP
|
||||
)
|
||||
|
||||
@onready var _main_panel: PanelContainer = %MainPanel
|
||||
@onready var _content_panel: PanelContainer = %ContentPanel
|
||||
@onready var _direct_content: Control = %DirectContent
|
||||
@onready var _list_content: Control = %ListContent
|
||||
@onready var _list_title: Label = %ListTitle
|
||||
@onready var _details_panel: PanelContainer = %DetailsPanel
|
||||
@onready var _address: LineEdit = %Address
|
||||
@onready var _address_label: Label = %AddressLabel
|
||||
@onready var _address_helper: Label = %AddressHelper
|
||||
|
|
@ -34,10 +41,11 @@ const DIRECT_WORKFLOW_HELP: String = (
|
|||
@onready var _name_helper: Label = %NameHelper
|
||||
@onready var _server_list: ItemList = %ServerList
|
||||
@onready var _details: Label = %Details
|
||||
@onready var _discover_button: Button = %DiscoverButton
|
||||
@onready var _direct_button: Button = %DirectButton
|
||||
@onready var _saved_button: Button = %SavedButton
|
||||
@onready var _recent_button: Button = %RecentButton
|
||||
@onready var _discover_button: OrganizerTab = %DiscoverButton
|
||||
@onready var _friends_button: OrganizerTab = %FriendsButton
|
||||
@onready var _direct_button: OrganizerTab = %DirectButton
|
||||
@onready var _saved_button: OrganizerTab = %SavedButton
|
||||
@onready var _recent_button: OrganizerTab = %RecentButton
|
||||
@onready var _join_button: Button = %JoinButton
|
||||
@onready var _refresh_button: Button = %RefreshButton
|
||||
@onready var _save_button: Button = %SaveButton
|
||||
|
|
@ -62,6 +70,8 @@ var _visible_entries: Array[SavedServerEntry] = []
|
|||
var _selected_entry: SavedServerEntry
|
||||
var _discovery_rooms: Array[Dictionary] = []
|
||||
var _selected_discovery_index: int = -1
|
||||
var _friend_entries: Array[Dictionary] = []
|
||||
var _selected_friend_index: int = -1
|
||||
var _discovery_refresh_timer: Timer
|
||||
var _editing_entry_id: String = ""
|
||||
var _name_entry_active: bool = false
|
||||
|
|
@ -69,24 +79,13 @@ var _delete_armed: bool = false
|
|||
var _connection_error_latched: bool = false
|
||||
var _pending_confirmation_endpoint: String = ""
|
||||
var _pending_confirmation_room: Dictionary = {}
|
||||
var _owns_pending_public_join: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
UtilityPageStyle.apply_page(self)
|
||||
var paper := get_node("Paper") as PanelContainer
|
||||
paper.add_theme_stylebox_override(
|
||||
"panel", UtilityPageStyle.panel_style()
|
||||
)
|
||||
for button: BaseButton in [
|
||||
_discover_button, _direct_button, _saved_button, _recent_button,
|
||||
_refresh_button, _join_button,
|
||||
_save_button, _edit_button, _favorite_button, _delete_button,
|
||||
_cancel_button, _back_button,
|
||||
]:
|
||||
UtilityPageStyle.apply_ocean_button(button)
|
||||
UtilityPageStyle.apply_ocean_line_edit(_address)
|
||||
UtilityPageStyle.apply_ocean_line_edit(_name_edit)
|
||||
_configure_style()
|
||||
_discover_button.pressed.connect(_set_mode.bind(Mode.DISCOVER))
|
||||
_friends_button.pressed.connect(_set_mode.bind(Mode.FRIENDS))
|
||||
_direct_button.pressed.connect(_set_mode.bind(Mode.DIRECT))
|
||||
_saved_button.pressed.connect(_set_mode.bind(Mode.SAVED))
|
||||
_recent_button.pressed.connect(_set_mode.bind(Mode.RECENT))
|
||||
|
|
@ -121,6 +120,96 @@ func _ready() -> void:
|
|||
hide()
|
||||
|
||||
|
||||
func _configure_style() -> void:
|
||||
UtilityPageStyle.apply_page(self)
|
||||
_main_panel.add_theme_stylebox_override(
|
||||
"panel",
|
||||
UtilityPageStyle.rounded_style(
|
||||
UtilityPageStyle.OCEAN_PANEL_MID,
|
||||
28,
|
||||
),
|
||||
)
|
||||
_content_panel.add_theme_stylebox_override(
|
||||
"panel",
|
||||
UtilityPageStyle.rounded_style(
|
||||
UtilityPageStyle.OCEAN_FIELD,
|
||||
20,
|
||||
),
|
||||
)
|
||||
_details_panel.add_theme_stylebox_override(
|
||||
"panel",
|
||||
UtilityPageStyle.row_style(),
|
||||
)
|
||||
_server_list.add_theme_stylebox_override(
|
||||
"panel",
|
||||
UtilityPageStyle.rounded_style(
|
||||
UtilityPageStyle.OCEAN_PANEL_MID,
|
||||
12,
|
||||
),
|
||||
)
|
||||
_server_list.add_theme_stylebox_override(
|
||||
"focus",
|
||||
StyleBoxEmpty.new(),
|
||||
)
|
||||
_server_list.add_theme_stylebox_override(
|
||||
"selected",
|
||||
UtilityPageStyle.row_style(true),
|
||||
)
|
||||
_server_list.add_theme_stylebox_override(
|
||||
"selected_focus",
|
||||
UtilityPageStyle.row_style(true),
|
||||
)
|
||||
_server_list.add_theme_color_override(
|
||||
"font_color",
|
||||
UtilityPageStyle.OCEAN_TEXT_PRIMARY,
|
||||
)
|
||||
_server_list.add_theme_color_override(
|
||||
"font_selected_color",
|
||||
UtilityPageStyle.OCEAN_TEXT_PRIMARY,
|
||||
)
|
||||
for node: Node in find_children("*", "Label", true, false):
|
||||
var label := node as Label
|
||||
label.add_theme_color_override(
|
||||
"font_color",
|
||||
UtilityPageStyle.OCEAN_TEXT_PRIMARY,
|
||||
)
|
||||
for node: Node in find_children("*", "Button", true, false):
|
||||
if node is OrganizerTab:
|
||||
continue
|
||||
UtilityPageStyle.apply_ocean_button(node as BaseButton)
|
||||
UtilityPageStyle.apply_ocean_line_edit(_address)
|
||||
UtilityPageStyle.apply_ocean_line_edit(_name_edit)
|
||||
_join_button.add_theme_stylebox_override(
|
||||
"normal",
|
||||
UtilityPageStyle.ocean_button_style(
|
||||
UtilityPageStyle.GREEN,
|
||||
),
|
||||
)
|
||||
_delete_button.add_theme_stylebox_override(
|
||||
"normal",
|
||||
UtilityPageStyle.ocean_button_style(
|
||||
UtilityPageStyle.OCEAN_DANGER,
|
||||
),
|
||||
)
|
||||
_back_button.add_theme_stylebox_override(
|
||||
"normal",
|
||||
UtilityPageStyle.ocean_button_style(
|
||||
UtilityPageStyle.OCEAN_PANEL_DEEP,
|
||||
),
|
||||
)
|
||||
for secondary_label: Label in [
|
||||
_address_helper,
|
||||
_name_helper,
|
||||
_details,
|
||||
_status,
|
||||
_session_summary,
|
||||
]:
|
||||
secondary_label.add_theme_color_override(
|
||||
"font_color",
|
||||
UtilityPageStyle.OCEAN_TEXT_SECONDARY,
|
||||
)
|
||||
|
||||
|
||||
func setup(
|
||||
network_session: NetworkSession,
|
||||
saved_servers: SavedServerStore,
|
||||
|
|
@ -177,6 +266,19 @@ func setup(
|
|||
_discovery.public_join_status_changed.connect(
|
||||
_on_public_join_status_changed
|
||||
)
|
||||
if not _discovery.friend_presence_updated.is_connected(
|
||||
_on_friend_presence_updated
|
||||
):
|
||||
_discovery.friend_presence_updated.connect(
|
||||
_on_friend_presence_updated
|
||||
)
|
||||
if not _discovery.social_status_changed.is_connected(
|
||||
_on_social_status_changed
|
||||
):
|
||||
_discovery.social_status_changed.connect(
|
||||
_on_social_status_changed
|
||||
)
|
||||
_friend_entries = _discovery.get_friend_presence()
|
||||
_refresh()
|
||||
|
||||
|
||||
|
|
@ -232,7 +334,9 @@ func confirm_pending_join() -> bool:
|
|||
var room: Dictionary = _pending_confirmation_room.duplicate(true)
|
||||
cancel_pending_join_confirmation()
|
||||
if not room.is_empty():
|
||||
_owns_pending_public_join = true
|
||||
if _discovery == null or not _discovery.prepare_public_join(room):
|
||||
_owns_pending_public_join = false
|
||||
_set_status("Could not prepare the public connection.", true)
|
||||
return false
|
||||
return true
|
||||
|
|
@ -244,15 +348,19 @@ func confirm_pending_join() -> bool:
|
|||
func _set_mode(mode: Mode, clear_connection_error: bool = true) -> void:
|
||||
if clear_connection_error:
|
||||
_connection_error_latched = false
|
||||
_set_status(_default_mode_status(mode))
|
||||
_mode = mode
|
||||
_selected_entry = null
|
||||
_selected_discovery_index = -1
|
||||
_selected_friend_index = -1
|
||||
_clear_edit_state()
|
||||
_refresh_entries()
|
||||
if mode == Mode.DISCOVER and not _discovery_rooms.is_empty():
|
||||
_select_discovery_index(0)
|
||||
elif mode == Mode.FRIENDS and not _friend_entries.is_empty():
|
||||
_select_friend_index(0)
|
||||
_refresh()
|
||||
if mode == Mode.DISCOVER:
|
||||
if mode in [Mode.DISCOVER, Mode.FRIENDS]:
|
||||
_discovery_refresh_timer.start()
|
||||
_request_discovery_refresh()
|
||||
else:
|
||||
|
|
@ -262,6 +370,19 @@ func _set_mode(mode: Mode, clear_connection_error: bool = true) -> void:
|
|||
_defer_focus_control(_address if mode == Mode.DIRECT else _server_list)
|
||||
|
||||
|
||||
func _default_mode_status(mode: Mode) -> String:
|
||||
match mode:
|
||||
Mode.FRIENDS:
|
||||
return "friend status is live and not stored by discovery"
|
||||
Mode.DIRECT:
|
||||
return "direct connection • default port 7777"
|
||||
Mode.SAVED:
|
||||
return "saved servers are stored on this device"
|
||||
Mode.RECENT:
|
||||
return "recent connections are stored on this device"
|
||||
return "looking for public rooms…"
|
||||
|
||||
|
||||
func _request_join() -> void:
|
||||
_connection_error_latched = false
|
||||
if _network_session.state in [
|
||||
|
|
@ -271,8 +392,22 @@ func _request_join() -> void:
|
|||
_network_session.reset_failure()
|
||||
var endpoint_text: String = _address.text
|
||||
var discovery_room: Dictionary = {}
|
||||
if _mode == Mode.DISCOVER:
|
||||
var room: Dictionary = _selected_discovery_room()
|
||||
if _mode in [Mode.DISCOVER, Mode.FRIENDS]:
|
||||
var room: Dictionary = (
|
||||
_selected_discovery_room()
|
||||
if _mode == Mode.DISCOVER
|
||||
else _selected_friend_room()
|
||||
)
|
||||
if _mode == Mode.FRIENDS:
|
||||
var friend := _selected_friend()
|
||||
if not bool(friend.get("online", false)):
|
||||
_set_status("This person needs to be online to do this.", true)
|
||||
return
|
||||
if room.is_empty():
|
||||
_set_status(
|
||||
"This friend is online but is not in a joinable room.", true
|
||||
)
|
||||
return
|
||||
if _discovery != null and _discovery.is_own_room(room):
|
||||
_set_status("You are already hosting this room.", true)
|
||||
return
|
||||
|
|
@ -296,7 +431,9 @@ func _request_join() -> void:
|
|||
join_confirmation_requested.emit(endpoint.normalized_display)
|
||||
return
|
||||
if not discovery_room.is_empty():
|
||||
_owns_pending_public_join = true
|
||||
if not _discovery.prepare_public_join(discovery_room):
|
||||
_owns_pending_public_join = false
|
||||
_set_status("Could not prepare the public connection.", true)
|
||||
return
|
||||
_set_status("Connecting…")
|
||||
|
|
@ -305,7 +442,10 @@ func _request_join() -> void:
|
|||
|
||||
|
||||
func _on_public_join_prepared(endpoint_text: String) -> void:
|
||||
if _mode != Mode.DISCOVER or not is_visible_in_tree():
|
||||
if not _owns_pending_public_join:
|
||||
return
|
||||
_owns_pending_public_join = false
|
||||
if _mode not in [Mode.DISCOVER, Mode.FRIENDS] or not is_visible_in_tree():
|
||||
return
|
||||
var endpoint: ConnectionEndpoint = EndpointParser.parse(endpoint_text)
|
||||
if not endpoint.is_valid():
|
||||
|
|
@ -317,8 +457,13 @@ func _on_public_join_prepared(endpoint_text: String) -> void:
|
|||
|
||||
|
||||
func _on_public_join_status_changed(message: String, is_error: bool) -> void:
|
||||
if _mode == Mode.DISCOVER and is_visible_in_tree():
|
||||
if (
|
||||
_owns_pending_public_join
|
||||
and _mode in [Mode.DISCOVER, Mode.FRIENDS]
|
||||
and is_visible_in_tree()
|
||||
):
|
||||
if is_error:
|
||||
_owns_pending_public_join = false
|
||||
_connection_error_latched = true
|
||||
elif _connection_error_latched:
|
||||
return
|
||||
|
|
@ -473,6 +618,11 @@ func _on_list_item_selected(index: int) -> void:
|
|||
return
|
||||
_refresh()
|
||||
return
|
||||
if _mode == Mode.FRIENDS:
|
||||
if not _select_friend_index(index):
|
||||
return
|
||||
_refresh()
|
||||
return
|
||||
if index < 0 or index >= _visible_entries.size():
|
||||
return
|
||||
_selected_entry = _visible_entries[index]
|
||||
|
|
@ -518,6 +668,8 @@ func _restore_entry_selection_and_focus(
|
|||
|
||||
func _current_mode_button() -> Button:
|
||||
match _mode:
|
||||
Mode.FRIENDS:
|
||||
return _friends_button
|
||||
Mode.DIRECT:
|
||||
return _direct_button
|
||||
Mode.SAVED:
|
||||
|
|
@ -536,6 +688,15 @@ func _select_discovery_index(index: int) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func _select_friend_index(index: int) -> bool:
|
||||
if index < 0 or index >= _friend_entries.size():
|
||||
return false
|
||||
_selected_friend_index = index
|
||||
_selected_entry = null
|
||||
_server_list.select(index)
|
||||
return true
|
||||
|
||||
|
||||
func _refresh_entries() -> void:
|
||||
_visible_entries.clear()
|
||||
_server_list.clear()
|
||||
|
|
@ -556,6 +717,25 @@ func _refresh_entries() -> void:
|
|||
]
|
||||
)
|
||||
return
|
||||
if _mode == Mode.FRIENDS:
|
||||
for friend: Dictionary in _friend_entries:
|
||||
var room: Dictionary = (
|
||||
friend.get("room", {})
|
||||
if typeof(friend.get("room", {})) == TYPE_DICTIONARY
|
||||
else {}
|
||||
)
|
||||
var state := "offline"
|
||||
if bool(friend.get("online", false)):
|
||||
state = (
|
||||
"playing in %s"
|
||||
% str(room.get("room_name", "a public room"))
|
||||
if not room.is_empty()
|
||||
else "online"
|
||||
)
|
||||
_server_list.add_item("%s — %s" % [
|
||||
str(friend.get("display_name", "Player")), state,
|
||||
])
|
||||
return
|
||||
if _saved_servers == null or _mode == Mode.DIRECT:
|
||||
return
|
||||
_visible_entries = (
|
||||
|
|
@ -594,15 +774,24 @@ func _refresh() -> void:
|
|||
] or (_discovery != null and _discovery.is_public_join_preparing())
|
||||
var direct: bool = _mode == Mode.DIRECT
|
||||
var discovery_mode: bool = _mode == Mode.DISCOVER
|
||||
var friends_mode: bool = _mode == Mode.FRIENDS
|
||||
var selected: bool = (
|
||||
_selected_discovery_index >= 0
|
||||
if discovery_mode
|
||||
else _selected_friend_index >= 0
|
||||
if friends_mode
|
||||
else _selected_entry != null
|
||||
)
|
||||
_discover_button.button_pressed = discovery_mode
|
||||
_direct_button.button_pressed = direct
|
||||
_saved_button.button_pressed = _mode == Mode.SAVED
|
||||
_recent_button.button_pressed = _mode == Mode.RECENT
|
||||
_discover_button.set_selected(discovery_mode, false)
|
||||
_friends_button.set_selected(friends_mode, false)
|
||||
_direct_button.set_selected(direct, false)
|
||||
_saved_button.set_selected(_mode == Mode.SAVED, false)
|
||||
_recent_button.set_selected(_mode == Mode.RECENT, false)
|
||||
var direct_content_visible: bool = direct or _name_entry_active
|
||||
var list_content_visible: bool = not direct_content_visible
|
||||
_direct_content.visible = direct_content_visible
|
||||
_list_content.visible = list_content_visible
|
||||
_list_title.text = _current_list_title()
|
||||
_address.visible = direct or _name_entry_active
|
||||
_address_label.visible = _address.visible
|
||||
_address_helper.visible = _address.visible
|
||||
|
|
@ -613,15 +802,20 @@ func _refresh() -> void:
|
|||
_name_edit.visible = _name_entry_active
|
||||
_name_label.visible = _name_entry_active
|
||||
_name_helper.visible = _name_entry_active
|
||||
_server_list.visible = not direct and not _name_entry_active
|
||||
_details.visible = not direct and not _name_entry_active
|
||||
_server_list.visible = list_content_visible
|
||||
_details_panel.visible = list_content_visible
|
||||
_details.visible = list_content_visible
|
||||
_join_button.disabled = (
|
||||
connecting
|
||||
or (not direct and not selected)
|
||||
or (
|
||||
discovery_mode
|
||||
(discovery_mode or friends_mode)
|
||||
and selected
|
||||
and _discovery_room_is_full(_selected_discovery_room())
|
||||
and _discovery_room_is_full(
|
||||
_selected_discovery_room()
|
||||
if discovery_mode
|
||||
else _selected_friend_room()
|
||||
)
|
||||
)
|
||||
or (
|
||||
discovery_mode
|
||||
|
|
@ -629,16 +823,19 @@ func _refresh() -> void:
|
|||
and _discovery != null
|
||||
and _discovery.is_own_room(_selected_discovery_room())
|
||||
)
|
||||
or (friends_mode and selected and _selected_friend_room().is_empty())
|
||||
)
|
||||
_join_button.text = "join now" if direct else "join"
|
||||
_refresh_button.visible = (
|
||||
(discovery_mode or friends_mode) and not _name_entry_active
|
||||
)
|
||||
_join_button.text = "join\nnow" if direct else "join"
|
||||
_refresh_button.visible = discovery_mode and not _name_entry_active
|
||||
_save_button.visible = (
|
||||
direct or _mode == Mode.RECENT or _name_entry_active
|
||||
)
|
||||
_save_button.disabled = connecting or (
|
||||
_mode == Mode.RECENT and not selected and not _name_entry_active
|
||||
)
|
||||
_save_button.text = "save" if _name_entry_active else "save\nserver"
|
||||
_save_button.text = "save" if _name_entry_active else "save server"
|
||||
_edit_button.visible = _mode == Mode.SAVED and selected
|
||||
_favorite_button.visible = _mode == Mode.SAVED and selected
|
||||
_favorite_button.text = (
|
||||
|
|
@ -676,16 +873,22 @@ func _refresh() -> void:
|
|||
_details.text = (
|
||||
_format_discovery_details(_selected_discovery_room())
|
||||
if discovery_mode
|
||||
else _format_friend_details(_selected_friend())
|
||||
if friends_mode
|
||||
else _format_entry_details(_selected_entry)
|
||||
)
|
||||
elif (
|
||||
_discovery_rooms.is_empty()
|
||||
if discovery_mode
|
||||
else _friend_entries.is_empty()
|
||||
if friends_mode
|
||||
else _visible_entries.is_empty()
|
||||
):
|
||||
_details.text = (
|
||||
"No public rooms are available."
|
||||
if discovery_mode
|
||||
else "No friends added yet."
|
||||
if friends_mode
|
||||
else "No saved servers yet."
|
||||
if _mode == Mode.SAVED
|
||||
else "No recent connections yet."
|
||||
|
|
@ -702,9 +905,21 @@ func _refresh() -> void:
|
|||
_configure_controller_navigation()
|
||||
|
||||
|
||||
func _current_list_title() -> String:
|
||||
match _mode:
|
||||
Mode.FRIENDS:
|
||||
return "friends"
|
||||
Mode.SAVED:
|
||||
return "saved servers"
|
||||
Mode.RECENT:
|
||||
return "recent connections"
|
||||
return "public rooms"
|
||||
|
||||
|
||||
func _configure_controller_navigation() -> void:
|
||||
var mode_buttons: Array[Control] = [
|
||||
_discover_button,
|
||||
_friends_button,
|
||||
_direct_button,
|
||||
_saved_button,
|
||||
_recent_button,
|
||||
|
|
@ -722,7 +937,6 @@ func _configure_controller_navigation() -> void:
|
|||
_favorite_button,
|
||||
_delete_button,
|
||||
_cancel_button,
|
||||
_back_button,
|
||||
]:
|
||||
if _controller_focus_eligible(control):
|
||||
action_controls.append(control)
|
||||
|
|
@ -730,6 +944,7 @@ func _configure_controller_navigation() -> void:
|
|||
all_controls.append_array(mode_buttons)
|
||||
all_controls.append_array(content_controls)
|
||||
all_controls.append_array(action_controls)
|
||||
all_controls.append(_back_button)
|
||||
for control: Control in all_controls:
|
||||
control.focus_mode = Control.FOCUS_ALL
|
||||
for control: Control in [
|
||||
|
|
@ -743,10 +958,10 @@ func _configure_controller_navigation() -> void:
|
|||
_favorite_button,
|
||||
_delete_button,
|
||||
_cancel_button,
|
||||
_back_button,
|
||||
]:
|
||||
if control not in all_controls:
|
||||
control.focus_mode = Control.FOCUS_NONE
|
||||
_back_button.focus_mode = Control.FOCUS_ALL
|
||||
var primary_content: Control = (
|
||||
content_controls.front()
|
||||
if not content_controls.is_empty()
|
||||
|
|
@ -775,7 +990,7 @@ func _configure_controller_navigation() -> void:
|
|||
if index < content_controls.size() - 1
|
||||
else action_controls.front()
|
||||
if not action_controls.is_empty()
|
||||
else content
|
||||
else _back_button
|
||||
)
|
||||
_set_controller_neighbors(content, content, content, above, below)
|
||||
for index: int in action_controls.size():
|
||||
|
|
@ -787,8 +1002,22 @@ func _configure_controller_navigation() -> void:
|
|||
content_controls.back()
|
||||
if not content_controls.is_empty()
|
||||
else mode_buttons[int(_mode)],
|
||||
action,
|
||||
_back_button,
|
||||
)
|
||||
var back_above: Control = (
|
||||
action_controls.back()
|
||||
if not action_controls.is_empty()
|
||||
else content_controls.back()
|
||||
if not content_controls.is_empty()
|
||||
else mode_buttons[int(_mode)]
|
||||
)
|
||||
_set_controller_neighbors(
|
||||
_back_button,
|
||||
_back_button,
|
||||
_back_button,
|
||||
back_above,
|
||||
_back_button,
|
||||
)
|
||||
ControllerFocusNavigationType.configure_traversal(all_controls)
|
||||
_recover_controller_focus(all_controls, primary_content)
|
||||
|
||||
|
|
@ -920,6 +1149,29 @@ func _format_discovery_details(room: Dictionary) -> String:
|
|||
return "\n".join(lines)
|
||||
|
||||
|
||||
func _format_friend_details(friend: Dictionary) -> String:
|
||||
if friend.is_empty():
|
||||
return "Select a friend."
|
||||
var room := _selected_friend_room()
|
||||
var lines: Array[String] = [
|
||||
"Friend: %s" % str(friend.get("display_name", "Player")),
|
||||
"Status: %s" % (
|
||||
"online" if bool(friend.get("online", false)) else "offline"
|
||||
),
|
||||
]
|
||||
if not room.is_empty():
|
||||
lines.append("Room: %s" % str(room.get("room_name", "Public room")))
|
||||
lines.append("Players: %d / %d" % [
|
||||
int(room.get("current_players", 0)),
|
||||
int(room.get("max_players", 0)),
|
||||
])
|
||||
elif bool(friend.get("online", false)):
|
||||
lines.append("This friend is not in a joinable public room.")
|
||||
else:
|
||||
lines.append("This person needs to be online to join them.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
func _selected_discovery_room() -> Dictionary:
|
||||
if (
|
||||
_selected_discovery_index < 0
|
||||
|
|
@ -929,6 +1181,25 @@ func _selected_discovery_room() -> Dictionary:
|
|||
return _discovery_rooms[_selected_discovery_index]
|
||||
|
||||
|
||||
func _selected_friend() -> Dictionary:
|
||||
if (
|
||||
_selected_friend_index < 0
|
||||
or _selected_friend_index >= _friend_entries.size()
|
||||
):
|
||||
return {}
|
||||
return _friend_entries[_selected_friend_index]
|
||||
|
||||
|
||||
func _selected_friend_room() -> Dictionary:
|
||||
var friend := _selected_friend()
|
||||
var room: Variant = friend.get("room", {})
|
||||
return (
|
||||
(room as Dictionary)
|
||||
if typeof(room) == TYPE_DICTIONARY
|
||||
else {}
|
||||
)
|
||||
|
||||
|
||||
func _discovery_room_is_full(room: Dictionary) -> bool:
|
||||
if room.is_empty():
|
||||
return false
|
||||
|
|
@ -938,11 +1209,14 @@ func _discovery_room_is_full(room: Dictionary) -> bool:
|
|||
func _request_discovery_refresh() -> void:
|
||||
if (
|
||||
_discovery == null
|
||||
or _mode != Mode.DISCOVER
|
||||
or _mode not in [Mode.DISCOVER, Mode.FRIENDS]
|
||||
or not is_visible_in_tree()
|
||||
):
|
||||
return
|
||||
_discovery.request_rooms()
|
||||
if _mode == Mode.DISCOVER:
|
||||
_discovery.request_rooms()
|
||||
else:
|
||||
_discovery.request_friend_presence()
|
||||
|
||||
|
||||
func _on_discovery_rooms_updated(rooms: Array[Dictionary]) -> void:
|
||||
|
|
@ -972,6 +1246,30 @@ func _on_discovery_status_changed(message: String, is_error: bool) -> void:
|
|||
_set_status(message, is_error)
|
||||
|
||||
|
||||
func _on_friend_presence_updated(friends: Array[Dictionary]) -> void:
|
||||
var selected_fingerprint := str(
|
||||
_selected_friend().get("fingerprint", "")
|
||||
)
|
||||
_friend_entries = friends.duplicate(true)
|
||||
_selected_friend_index = -1
|
||||
if _mode != Mode.FRIENDS:
|
||||
return
|
||||
_refresh_entries()
|
||||
if not selected_fingerprint.is_empty():
|
||||
for index: int in _friend_entries.size():
|
||||
if str(_friend_entries[index].get("fingerprint", "")) == selected_fingerprint:
|
||||
_select_friend_index(index)
|
||||
break
|
||||
if _selected_friend_index < 0 and not _friend_entries.is_empty():
|
||||
_select_friend_index(0)
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_social_status_changed(message: String, is_error: bool) -> void:
|
||||
if _mode == Mode.FRIENDS and is_visible_in_tree():
|
||||
_set_status(message, is_error)
|
||||
|
||||
|
||||
func _format_result_code(result_code: String) -> String:
|
||||
match result_code.strip_edges().to_upper():
|
||||
"SUCCESS":
|
||||
|
|
|
|||
|
|
@ -1,76 +1,10 @@
|
|||
[gd_scene load_steps=12 format=3]
|
||||
[gd_scene load_steps=5 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/network/join_game_page.gd" id="1_script"]
|
||||
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
||||
[ext_resource type="Script" path="res://ui/components/organizer_tab.gd" id="3_tab"]
|
||||
[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_confirmation_page.tscn" id="4_confirmation"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_paper"]
|
||||
bg_color = Color(0.051, 0.173, 0.227, 1)
|
||||
corner_radius_top_left = 54
|
||||
corner_radius_top_right = 46
|
||||
corner_radius_bottom_right = 58
|
||||
corner_radius_bottom_left = 48
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_list"]
|
||||
bg_color = Color(0.071, 0.247, 0.306, 1)
|
||||
corner_radius_top_left = 12
|
||||
corner_radius_top_right = 9
|
||||
corner_radius_bottom_right = 13
|
||||
corner_radius_bottom_left = 10
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_selected"]
|
||||
bg_color = Color(0.137, 0.525, 0.592, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 7
|
||||
corner_radius_bottom_right = 9
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBox_focus"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_input"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 7.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 7.0
|
||||
bg_color = Color(0.031, 0.122, 0.169, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 7
|
||||
corner_radius_bottom_right = 9
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_input_focus"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 7.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 7.0
|
||||
bg_color = Color(0.137, 0.525, 0.592, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 7
|
||||
corner_radius_bottom_right = 9
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_input_read_only"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 7.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 7.0
|
||||
bg_color = Color(0.031, 0.122, 0.169, 0.82)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 7
|
||||
corner_radius_bottom_right = 9
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_status"]
|
||||
content_margin_left = 10.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 10.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(0.071, 0.247, 0.306, 0.9)
|
||||
corner_radius_top_left = 7
|
||||
corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 5
|
||||
|
||||
[node name="JoinGamePage" type="Control"]
|
||||
visible = false
|
||||
layout_mode = 3
|
||||
|
|
@ -82,243 +16,322 @@ grow_vertical = 2
|
|||
theme = ExtResource("2_theme")
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Paper" type="PanelContainer" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 230.0
|
||||
offset_top = 64.0
|
||||
offset_right = 1050.0
|
||||
offset_bottom = 656.0
|
||||
theme_override_styles/panel = SubResource("StyleBox_paper")
|
||||
[node name="MainPanel" type="PanelContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -440.0
|
||||
offset_top = -330.0
|
||||
offset_right = 440.0
|
||||
offset_bottom = 330.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Margin" type="MarginContainer" parent="Paper"]
|
||||
[node name="OuterMargin" type="MarginContainer" parent="MainPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 54
|
||||
theme_override_constants/margin_top = 30
|
||||
theme_override_constants/margin_right = 54
|
||||
theme_override_constants/margin_bottom = 30
|
||||
theme_override_constants/margin_left = 24
|
||||
theme_override_constants/margin_top = 18
|
||||
theme_override_constants/margin_right = 24
|
||||
theme_override_constants/margin_bottom = 18
|
||||
|
||||
[node name="Layout" type="VBoxContainer" parent="Paper/Margin"]
|
||||
[node name="Layout" type="VBoxContainer" parent="MainPanel/OuterMargin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
alignment = 1
|
||||
theme_override_constants/separation = -26
|
||||
|
||||
[node name="Title" type="Label" parent="Paper/Margin/Layout"]
|
||||
[node name="Heading" type="Label" parent="MainPanel/OuterMargin/Layout"]
|
||||
custom_minimum_size = Vector2(0, 66)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.902, 0.969, 0.969, 1)
|
||||
theme_override_font_sizes/font_size = 30
|
||||
text = "join game"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Modes" type="HBoxContainer" parent="Paper/Margin/Layout"]
|
||||
[node name="TabBar" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 52)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
theme_override_constants/separation = 8
|
||||
alignment = 1
|
||||
|
||||
[node name="DiscoverButton" type="Button" parent="Paper/Margin/Layout/Modes"]
|
||||
[node name="DiscoverButton" type="Button" parent="MainPanel/OuterMargin/Layout/TabBar"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 72)
|
||||
custom_minimum_size = Vector2(165, 52)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
focus_mode = 2
|
||||
text = "discover"
|
||||
script = ExtResource("3_tab")
|
||||
|
||||
[node name="DirectButton" type="Button" parent="Paper/Margin/Layout/Modes"]
|
||||
[node name="FriendsButton" type="Button" parent="MainPanel/OuterMargin/Layout/TabBar"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 72)
|
||||
custom_minimum_size = Vector2(140, 52)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
focus_mode = 2
|
||||
text = "friends"
|
||||
script = ExtResource("3_tab")
|
||||
palette_index = 1
|
||||
|
||||
[node name="DirectButton" type="Button" parent="MainPanel/OuterMargin/Layout/TabBar"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(140, 52)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "direct"
|
||||
script = ExtResource("3_tab")
|
||||
palette_index = 1
|
||||
|
||||
[node name="SavedButton" type="Button" parent="Paper/Margin/Layout/Modes"]
|
||||
[node name="SavedButton" type="Button" parent="MainPanel/OuterMargin/Layout/TabBar"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 72)
|
||||
custom_minimum_size = Vector2(140, 52)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
focus_mode = 2
|
||||
text = "saved"
|
||||
script = ExtResource("3_tab")
|
||||
palette_index = 2
|
||||
|
||||
[node name="RecentButton" type="Button" parent="Paper/Margin/Layout/Modes"]
|
||||
[node name="RecentButton" type="Button" parent="MainPanel/OuterMargin/Layout/TabBar"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 72)
|
||||
custom_minimum_size = Vector2(140, 52)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
focus_mode = 2
|
||||
text = "recent"
|
||||
script = ExtResource("3_tab")
|
||||
palette_index = 1
|
||||
|
||||
[node name="AddressLabel" type="Label" parent="Paper/Margin/Layout"]
|
||||
[node name="ContentPanel" type="PanelContainer" parent="MainPanel/OuterMargin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 474)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.902, 0.969, 0.969, 1)
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ContentMargin" type="MarginContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 28
|
||||
theme_override_constants/margin_top = 20
|
||||
theme_override_constants/margin_right = 28
|
||||
theme_override_constants/margin_bottom = 20
|
||||
|
||||
[node name="ContentLayout" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="PageStack" type="Control" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ListContent" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="ListTitle" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/ListContent"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 28)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "public rooms"
|
||||
|
||||
[node name="ServerList" type="ItemList" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/ListContent"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 180)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 17
|
||||
text = "server address"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Address" type="LineEdit" parent="Paper/Margin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 42)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.96, 0.93, 0.82, 1)
|
||||
theme_override_colors/font_uneditable_color = Color(0.624, 0.812, 0.824, 1)
|
||||
theme_override_colors/font_selected_color = Color(1, 0.98, 0.9, 1)
|
||||
theme_override_colors/font_placeholder_color = Color(0.624, 0.812, 0.824, 0.78)
|
||||
theme_override_colors/caret_color = Color(0.902, 0.969, 0.969, 1)
|
||||
theme_override_colors/selection_color = Color(0.2, 0.48, 0.59, 0.9)
|
||||
theme_override_font_sizes/font_size = 19
|
||||
theme_override_styles/normal = SubResource("StyleBox_input")
|
||||
theme_override_styles/focus = SubResource("StyleBox_input_focus")
|
||||
theme_override_styles/read_only = SubResource("StyleBox_input_read_only")
|
||||
placeholder_text = "example.net or 192.168.1.50:7777"
|
||||
alignment = 1
|
||||
max_length = 300
|
||||
|
||||
[node name="AddressHelper" type="Label" parent="Paper/Margin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.624, 0.812, 0.824, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted; include the port shown by a host when it differs.\nJoin Now connects once; Save Server stores this address locally."
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="NameLabel" type="Label" parent="Paper/Margin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.902, 0.969, 0.969, 1)
|
||||
theme_override_font_sizes/font_size = 17
|
||||
text = "server name"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="NameEdit" type="LineEdit" parent="Paper/Margin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 42)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.96, 0.93, 0.82, 1)
|
||||
theme_override_colors/font_uneditable_color = Color(0.624, 0.812, 0.824, 1)
|
||||
theme_override_colors/font_selected_color = Color(1, 0.98, 0.9, 1)
|
||||
theme_override_colors/font_placeholder_color = Color(0.624, 0.812, 0.824, 0.78)
|
||||
theme_override_colors/caret_color = Color(0.902, 0.969, 0.969, 1)
|
||||
theme_override_colors/selection_color = Color(0.2, 0.48, 0.59, 0.9)
|
||||
theme_override_font_sizes/font_size = 19
|
||||
theme_override_styles/normal = SubResource("StyleBox_input")
|
||||
theme_override_styles/focus = SubResource("StyleBox_input_focus")
|
||||
theme_override_styles/read_only = SubResource("StyleBox_input_read_only")
|
||||
placeholder_text = "Friend's server"
|
||||
alignment = 1
|
||||
max_length = 80
|
||||
|
||||
[node name="NameHelper" type="Label" parent="Paper/Margin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.624, 0.812, 0.824, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "Only visible on this device."
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="ServerList" type="ItemList" parent="Paper/Margin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 145)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.902, 0.969, 0.969, 1)
|
||||
theme_override_colors/font_selected_color = Color(0.902, 0.969, 0.969, 1)
|
||||
theme_override_colors/guide_color = Color(0.624, 0.812, 0.824, 0.22)
|
||||
theme_override_font_sizes/font_size = 17
|
||||
theme_override_styles/panel = SubResource("StyleBox_list")
|
||||
theme_override_styles/focus = SubResource("StyleBox_focus")
|
||||
theme_override_styles/selected = SubResource("StyleBox_selected")
|
||||
theme_override_styles/selected_focus = SubResource("StyleBox_selected")
|
||||
allow_reselect = true
|
||||
same_column_width = true
|
||||
|
||||
[node name="Details" type="Label" parent="Paper/Margin/Layout"]
|
||||
[node name="DetailsPanel" type="PanelContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/ListContent"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 92)
|
||||
custom_minimum_size = Vector2(0, 100)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.902, 0.969, 0.969, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
|
||||
[node name="Details" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/ListContent/DetailsPanel"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "Select a server."
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="Status" type="Label" parent="Paper/Margin/Layout"]
|
||||
[node name="DirectContent" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 38)
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="AddressLabel" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/DirectContent"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 28)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.902, 0.969, 0.969, 1)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
theme_override_styles/normal = SubResource("StyleBox_status")
|
||||
text = "Direct UDP connection • default port 7777"
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "server address"
|
||||
|
||||
[node name="Address" type="LineEdit" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/DirectContent"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
theme_override_font_sizes/font_size = 19
|
||||
placeholder_text = "example.net or 192.168.1.50:7777"
|
||||
alignment = 1
|
||||
max_length = 300
|
||||
|
||||
[node name="AddressHelper" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/DirectContent"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 48)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted; include the port shown by a host when it differs.\nJoin Now connects once; Save Server stores this address locally."
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="SessionSummary" type="Label" parent="Paper/Margin/Layout"]
|
||||
[node name="NameLabel" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/DirectContent"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 24)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "server name"
|
||||
|
||||
[node name="NameEdit" type="LineEdit" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/DirectContent"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
theme_override_font_sizes/font_size = 19
|
||||
placeholder_text = "Friend's server"
|
||||
alignment = 1
|
||||
max_length = 80
|
||||
|
||||
[node name="NameHelper" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/DirectContent"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.624, 0.812, 0.824, 1)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "1 / 8 players"
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "Only visible on this device."
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Actions" type="HBoxContainer" parent="Paper/Margin/Layout"]
|
||||
[node name="Spacer" type="Control" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/DirectContent"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 7
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="Actions" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
alignment = 1
|
||||
|
||||
[node name="RefreshButton" type="Button" parent="Paper/Margin/Layout/Actions"]
|
||||
[node name="RefreshButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/Actions"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(82, 72)
|
||||
custom_minimum_size = Vector2(120, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "refresh"
|
||||
|
||||
[node name="JoinButton" type="Button" parent="Paper/Margin/Layout/Actions"]
|
||||
[node name="JoinButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/Actions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(82, 72)
|
||||
custom_minimum_size = Vector2(120, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "join"
|
||||
|
||||
[node name="SaveButton" type="Button" parent="Paper/Margin/Layout/Actions"]
|
||||
[node name="SaveButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/Actions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(82, 72)
|
||||
custom_minimum_size = Vector2(130, 46)
|
||||
layout_mode = 2
|
||||
text = "save\nserver"
|
||||
focus_mode = 2
|
||||
text = "save server"
|
||||
|
||||
[node name="EditButton" type="Button" parent="Paper/Margin/Layout/Actions"]
|
||||
[node name="EditButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/Actions"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(76, 68)
|
||||
custom_minimum_size = Vector2(110, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "edit"
|
||||
|
||||
[node name="FavoriteButton" type="Button" parent="Paper/Margin/Layout/Actions"]
|
||||
[node name="FavoriteButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/Actions"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(88, 68)
|
||||
custom_minimum_size = Vector2(130, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "favorite"
|
||||
|
||||
[node name="DeleteButton" type="Button" parent="Paper/Margin/Layout/Actions"]
|
||||
[node name="DeleteButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/Actions"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(82, 68)
|
||||
custom_minimum_size = Vector2(110, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "delete"
|
||||
|
||||
[node name="CancelButton" type="Button" parent="Paper/Margin/Layout/Actions"]
|
||||
[node name="CancelButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/Actions"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(76, 68)
|
||||
custom_minimum_size = Vector2(120, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "cancel"
|
||||
|
||||
[node name="BackButton" type="Button" parent="Paper/Margin/Layout/Actions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(76, 68)
|
||||
[node name="FooterGroup" type="MarginContainer" parent="MainPanel/OuterMargin/Layout"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_top = 34
|
||||
|
||||
[node name="FooterLayout" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/FooterGroup"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="InfoRow" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/FooterGroup/FooterLayout"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="Status" type="Label" parent="MainPanel/OuterMargin/Layout/FooterGroup/FooterLayout/InfoRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 24)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "looking for public rooms…"
|
||||
vertical_alignment = 1
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="SessionSummary" type="Label" parent="MainPanel/OuterMargin/Layout/FooterGroup/FooterLayout/InfoRow"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(210, 24)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "1 / 8 players"
|
||||
horizontal_alignment = 2
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="Footer" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/FooterGroup/FooterLayout"]
|
||||
layout_mode = 2
|
||||
alignment = 2
|
||||
|
||||
[node name="BackButton" type="Button" parent="MainPanel/OuterMargin/Layout/FooterGroup/FooterLayout/Footer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(150, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "back"
|
||||
|
||||
[node name="DeleteConfirmation" parent="." instance=ExtResource("4_confirmation")]
|
||||
|
|
|
|||
|
|
@ -100,6 +100,23 @@ func is_open() -> bool:
|
|||
return visible or _native_text_entry_is_active()
|
||||
|
||||
|
||||
func close_for_control(control: Control = null) -> bool:
|
||||
if visible and (control == null or _target == control):
|
||||
_close_keyboard(false)
|
||||
return true
|
||||
if not _uses_native_virtual_keyboard():
|
||||
return false
|
||||
var native_target: Control = _native_session_target()
|
||||
if native_target != null and (control == null or native_target == control):
|
||||
_close_native_keyboard()
|
||||
return true
|
||||
if control != null and control.has_focus() and _can_edit(control):
|
||||
_hide_native_keyboard()
|
||||
control.release_focus()
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func setup_controller_mapping(
|
||||
mapping_manager: ControllerMappingManagerType,
|
||||
) -> void:
|
||||
|
|
@ -110,7 +127,10 @@ func request_for_focused_control() -> bool:
|
|||
return request_for_control(get_viewport().gui_get_focus_owner())
|
||||
|
||||
|
||||
func request_for_control(control: Control = null) -> bool:
|
||||
func request_for_control(
|
||||
control: Control = null,
|
||||
preserve_caret: bool = false,
|
||||
) -> bool:
|
||||
if visible or not _can_edit(control):
|
||||
return false
|
||||
if _uses_native_virtual_keyboard():
|
||||
|
|
@ -118,7 +138,7 @@ func request_for_control(control: Control = null) -> bool:
|
|||
return true
|
||||
if not _is_available_for_controller():
|
||||
return false
|
||||
_open_for(control)
|
||||
_open_for(control, preserve_caret)
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -478,14 +498,20 @@ func _can_edit(control: Control) -> bool:
|
|||
return false
|
||||
|
||||
|
||||
func _open_for(control: Control) -> void:
|
||||
func _open_for(control: Control, preserve_caret: bool = false) -> void:
|
||||
_target = control
|
||||
_target_virtual_keyboard_enabled = bool(
|
||||
_target.get("virtual_keyboard_enabled")
|
||||
)
|
||||
_target.set("virtual_keyboard_enabled", false)
|
||||
_buffer = str(_target.get("text"))
|
||||
_buffer_caret = _buffer.length()
|
||||
if preserve_caret and _target is LineEdit:
|
||||
_buffer_caret = (_target as LineEdit).caret_column
|
||||
elif preserve_caret and _target is TextEdit:
|
||||
_buffer_caret = _text_edit_caret_offset(_target as TextEdit)
|
||||
else:
|
||||
_buffer_caret = _buffer.length()
|
||||
_buffer_caret = clampi(_buffer_caret, 0, _buffer.length())
|
||||
_set_target_caret(_buffer_caret)
|
||||
_page = Page.LOWER
|
||||
_last_focused_key = null
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ const MODERATION_KICK_ICON: Texture2D = preload(
|
|||
const MODERATION_MUTE_ICON: Texture2D = preload(
|
||||
"res://ui/icons/moderation_options/moderation_options_mute_light.png"
|
||||
)
|
||||
const FRIEND_ICON: Texture2D = preload(
|
||||
"res://ui/icons/player_options/friends.png"
|
||||
)
|
||||
const CLEAR_ART_ICON: Texture2D = preload(
|
||||
"res://ui/icons/player_options/clean.png"
|
||||
)
|
||||
const MODERATION_BUTTON_SIZE := Vector2(52.0, 40.0)
|
||||
const MODERATION_ICON_SIZE: int = 40
|
||||
|
||||
|
|
@ -61,11 +67,31 @@ func setup(
|
|||
_status.text = message
|
||||
_refresh()
|
||||
)
|
||||
_service.friend_action_finished.connect(func(_ok: bool, message: String) -> void:
|
||||
_status.text = message
|
||||
_refresh()
|
||||
)
|
||||
if _discovery != null:
|
||||
_discovery.host_settings_changed.connect(
|
||||
_on_host_settings_changed
|
||||
)
|
||||
_discovery.host_status_changed.connect(_on_host_status_changed)
|
||||
_discovery.friend_presence_updated.connect(
|
||||
func(_friends: Array[Dictionary]) -> void: _refresh()
|
||||
)
|
||||
_discovery.presence_sharing_changed.connect(
|
||||
func(_enabled: bool) -> void: _refresh()
|
||||
)
|
||||
_discovery.friend_invite_finished.connect(
|
||||
func(_ok: bool, message: String) -> void:
|
||||
_status.text = message
|
||||
_refresh()
|
||||
)
|
||||
_discovery.social_status_changed.connect(
|
||||
func(message: String, is_error: bool) -> void:
|
||||
if is_error or _current_tab == 1:
|
||||
_status.text = message
|
||||
)
|
||||
_refresh()
|
||||
|
||||
|
||||
|
|
@ -185,9 +211,9 @@ func _build() -> void:
|
|||
_tabs.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_tabs.add_theme_constant_override("separation", 10)
|
||||
tab_row.add_child(_tabs)
|
||||
for index: int in 3:
|
||||
for index: int in 4:
|
||||
var button := Button.new()
|
||||
button.text = ["players", "relationships", "banned"][index]
|
||||
button.text = ["players", "friends", "relationships", "banned"][index]
|
||||
button.toggle_mode = true
|
||||
button.pressed.connect(_select_tab.bind(index))
|
||||
UtilityPageStyle.apply_ocean_button(button)
|
||||
|
|
@ -322,7 +348,7 @@ func _set_host_toggle_state(
|
|||
|
||||
|
||||
func _select_tab(index: int) -> void:
|
||||
if index == 2 and (_service == null or not _service.is_local_moderator()):
|
||||
if index == 3 and (_service == null or not _service.is_local_moderator()):
|
||||
return
|
||||
_current_tab = index
|
||||
_refresh()
|
||||
|
|
@ -335,12 +361,12 @@ func _refresh() -> void:
|
|||
_count_label.text = "%d / %d connected" % [
|
||||
_service.get_connected_count(), _service.get_max_players(),
|
||||
]
|
||||
if _current_tab == 2 and not _service.is_local_moderator():
|
||||
if _current_tab == 3 and not _service.is_local_moderator():
|
||||
_current_tab = 0
|
||||
for index: int in _tabs.get_child_count():
|
||||
var button := _tabs.get_child(index) as Button
|
||||
button.button_pressed = index == _current_tab
|
||||
button.visible = index != 2 or _service.is_local_moderator()
|
||||
button.visible = index != 3 or _service.is_local_moderator()
|
||||
_refresh_host_settings()
|
||||
for child: Node in _list.get_children():
|
||||
child.queue_free()
|
||||
|
|
@ -348,8 +374,10 @@ func _refresh() -> void:
|
|||
0:
|
||||
_build_active_rows()
|
||||
1:
|
||||
_build_relationship_rows()
|
||||
_build_friend_rows()
|
||||
2:
|
||||
_build_relationship_rows()
|
||||
3:
|
||||
_build_ban_rows()
|
||||
if _controller_zone == ControllerZone.BODY and _body_controls().is_empty():
|
||||
_controller_zone = ControllerZone.TABS
|
||||
|
|
@ -520,6 +548,23 @@ func _build_active_player_row(entry: PlayerListEntry) -> void:
|
|||
actions.alignment = BoxContainer.ALIGNMENT_END
|
||||
actions.add_theme_constant_override("separation", 6)
|
||||
row.add_child(actions)
|
||||
var friend := Button.new()
|
||||
friend.disabled = entry.is_local_player or entry.is_friend or not entry.can_request_friend
|
||||
var friend_action := "friends" if entry.is_friend else "add friend"
|
||||
_configure_moderation_button(friend, FRIEND_ICON, friend_action)
|
||||
friend.tooltip_text = (
|
||||
"Already friends."
|
||||
if entry.is_friend
|
||||
else "This person needs to be online to do this."
|
||||
if not entry.can_request_friend
|
||||
else "Send a live friend request."
|
||||
)
|
||||
friend.pressed.connect(func() -> void:
|
||||
_service.send_friend_request(
|
||||
entry.peer_id, entry.full_fingerprint, entry.display_name
|
||||
)
|
||||
)
|
||||
actions.add_child(friend)
|
||||
var mute := Button.new()
|
||||
var mute_action: String = "unmute" if entry.muted else "mute"
|
||||
mute.disabled = entry.is_local_player
|
||||
|
|
@ -539,14 +584,9 @@ func _build_active_player_row(entry: PlayerListEntry) -> void:
|
|||
operator.custom_minimum_size = MODERATION_BUTTON_SIZE
|
||||
actions.add_child(operator)
|
||||
var clear_art := Button.new()
|
||||
clear_art.text = "clear art"
|
||||
clear_art.disabled = not entry.can_clear_art
|
||||
clear_art.tooltip_text = (
|
||||
"Remove every shared artwork this player participated in."
|
||||
)
|
||||
_configure_moderation_button(clear_art, CLEAR_ART_ICON, "scrub art")
|
||||
clear_art.pressed.connect(_confirm_clear_art.bind(entry))
|
||||
UtilityPageStyle.apply_compact_ocean_button(clear_art)
|
||||
clear_art.custom_minimum_size = Vector2(84.0, 40.0)
|
||||
actions.add_child(clear_art)
|
||||
var kick := Button.new()
|
||||
kick.disabled = not entry.can_kick
|
||||
|
|
@ -661,6 +701,104 @@ func _build_relationship_rows() -> void:
|
|||
row.add_child(unmute)
|
||||
|
||||
|
||||
func _build_friend_rows() -> void:
|
||||
_build_presence_controls()
|
||||
var friends: Array[Dictionary] = (
|
||||
_discovery.get_friend_presence()
|
||||
if _discovery != null
|
||||
else []
|
||||
)
|
||||
if friends.is_empty():
|
||||
_add_empty("No friends added yet. Add someone while you are in a room together.")
|
||||
return
|
||||
for friend: Dictionary in friends:
|
||||
var row := _make_row()
|
||||
var fingerprint := str(friend.get("fingerprint", ""))
|
||||
var display_name := str(friend.get("display_name", "Player"))
|
||||
var room: Dictionary = (
|
||||
friend.get("room", {})
|
||||
if typeof(friend.get("room", {})) == TYPE_DICTIONARY
|
||||
else {}
|
||||
)
|
||||
var label := Label.new()
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
label.clip_text = true
|
||||
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
label.text = "%s · %s %s" % [
|
||||
display_name,
|
||||
NetworkIdentityCrypto.compact_suffix(fingerprint),
|
||||
"playing in %s" % str(room.get("room_name", "a public room"))
|
||||
if not room.is_empty()
|
||||
else "Online"
|
||||
if bool(friend.get("online", false))
|
||||
else "Offline",
|
||||
]
|
||||
label.tooltip_text = NetworkIdentityCrypto.format_fingerprint(fingerprint)
|
||||
label.add_theme_color_override(
|
||||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||
)
|
||||
row.add_child(label)
|
||||
var invite := Button.new()
|
||||
invite.text = "invite"
|
||||
invite.tooltip_text = (
|
||||
"Invite this friend to your listed public room."
|
||||
if bool(friend.get("online", false))
|
||||
else "This person needs to be online to do this."
|
||||
)
|
||||
invite.pressed.connect(func() -> void:
|
||||
_discovery.send_friend_invite(fingerprint)
|
||||
)
|
||||
UtilityPageStyle.apply_compact_ocean_button(invite)
|
||||
row.add_child(invite)
|
||||
var remove := Button.new()
|
||||
remove.text = "remove"
|
||||
remove.pressed.connect(func() -> void:
|
||||
_confirm(
|
||||
"Remove %s from your friends?" % display_name,
|
||||
func() -> void:
|
||||
_service.remove_friend(fingerprint, display_name),
|
||||
)
|
||||
)
|
||||
UtilityPageStyle.apply_compact_ocean_button(remove)
|
||||
row.add_child(remove)
|
||||
var block := Button.new()
|
||||
block.text = "block"
|
||||
block.pressed.connect(func() -> void:
|
||||
_confirm(
|
||||
"Block %s?\nThis also removes the friendship." % display_name,
|
||||
func() -> void:
|
||||
_service.set_blocked(fingerprint, display_name, true),
|
||||
)
|
||||
)
|
||||
UtilityPageStyle.apply_compact_ocean_button(block)
|
||||
row.add_child(block)
|
||||
|
||||
|
||||
func _build_presence_controls() -> void:
|
||||
var row := _make_row()
|
||||
var label := Label.new()
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
label.text = "share online status with friends"
|
||||
label.add_theme_color_override(
|
||||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||
)
|
||||
row.add_child(label)
|
||||
var toggle := Button.new()
|
||||
var enabled := _discovery != null and _discovery.is_presence_sharing()
|
||||
toggle.text = "on" if enabled else "off"
|
||||
toggle.tooltip_text = (
|
||||
"Friends can see when you are online and whether your room is joinable."
|
||||
)
|
||||
toggle.disabled = _discovery == null or not _discovery.is_configured()
|
||||
toggle.pressed.connect(func() -> void:
|
||||
_discovery.set_presence_sharing(
|
||||
not _discovery.is_presence_sharing()
|
||||
)
|
||||
)
|
||||
UtilityPageStyle.apply_compact_ocean_button(toggle)
|
||||
row.add_child(toggle)
|
||||
|
||||
|
||||
func _build_ban_rows() -> void:
|
||||
var records := _service.get_bans()
|
||||
if records.is_empty():
|
||||
|
|
|
|||
731
ui/save_slots_page.gd
Normal file
731
ui/save_slots_page.gd
Normal file
|
|
@ -0,0 +1,731 @@
|
|||
class_name SaveSlotsPage
|
||||
extends Control
|
||||
|
||||
const PAGE_SAVES: StringName = &"saves"
|
||||
const PAGE_NEW: StringName = &"new"
|
||||
const WorldLayoutType = preload("res://world/world_layout.gd")
|
||||
const SaveManagerType = preload("res://save/player_save_manager.gd")
|
||||
const NewGameSetupPageType = preload("res://ui/new_game_setup_page.gd")
|
||||
const DialogControllerNavigationType = preload(
|
||||
"res://ui/file_dialog_controller_navigation.gd"
|
||||
)
|
||||
|
||||
signal back_requested
|
||||
signal play_requested(slot_id: String)
|
||||
signal create_requested(
|
||||
display_name: String,
|
||||
world_layout: StringName,
|
||||
world_seed: int,
|
||||
duplicate_source_slot_id: String,
|
||||
)
|
||||
|
||||
enum SeedMode {
|
||||
RANDOM,
|
||||
CUSTOM,
|
||||
}
|
||||
|
||||
@onready var _main_panel: PanelContainer = %MainPanel
|
||||
@onready var _content_panel: PanelContainer = %ContentPanel
|
||||
@onready var _saves_tab: OrganizerTab = %SavesTab
|
||||
@onready var _new_slot_tab: OrganizerTab = %NewSlotTab
|
||||
@onready var _saves_page: Control = %SavesPage
|
||||
@onready var _new_slot_page: Control = %NewSlotPage
|
||||
@onready var _slot_list: VBoxContainer = %SlotList
|
||||
@onready var _empty_slots_label: Label = %EmptySlotsLabel
|
||||
@onready var _slot_name_edit: LineEdit = %SelectedSlotName
|
||||
@onready var _slot_summary: RichTextLabel = %SlotSummary
|
||||
@onready var _play_button: Button = %PlaySlotButton
|
||||
@onready var _rename_button: Button = %RenameSlotButton
|
||||
@onready var _duplicate_button: Button = %DuplicateSlotButton
|
||||
@onready var _export_button: Button = %ExportSlotButton
|
||||
@onready var _delete_button: Button = %DeleteSlotButton
|
||||
@onready var _import_button: Button = %ImportSlotButton
|
||||
@onready var _new_slot_heading: Label = %NewSlotHeading
|
||||
@onready var _new_slot_name: LineEdit = %NewSlotName
|
||||
@onready var _generated_button: Button = %GeneratedButton
|
||||
@onready var _starter_button: Button = %StarterButton
|
||||
@onready var _world_description: Label = %WorldDescription
|
||||
@onready var _seed_section: VBoxContainer = %SeedSection
|
||||
@onready var _random_seed_button: Button = %RandomSeedButton
|
||||
@onready var _custom_seed_button: Button = %CustomSeedButton
|
||||
@onready var _seed_edit: LineEdit = %SeedEdit
|
||||
@onready var _seed_help: Label = %SeedHelp
|
||||
@onready var _create_button: Button = %CreateSlotButton
|
||||
@onready var _status: Label = %Status
|
||||
@onready var _back_button: Button = %BackButton
|
||||
|
||||
var _catalog: PlayerSaveSlotCatalog
|
||||
var _data_root: PlayerDataRoot
|
||||
var _interface_fonts: InterfaceFontController
|
||||
var _active_page_id: StringName = PAGE_SAVES
|
||||
var _selected_slot_id := ""
|
||||
var _duplicate_source_slot_id := ""
|
||||
var _world_layout: StringName = WorldLayoutType.GENERATED
|
||||
var _seed_mode: SeedMode = SeedMode.RANDOM
|
||||
var _random_seed: int = SaveManagerType.DEFAULT_WORLD_SEED
|
||||
var _slot_buttons: Array[Button] = []
|
||||
var _import_dialog: FileDialog
|
||||
var _export_dialog: FileDialog
|
||||
var _delete_dialog: ConfirmationDialog
|
||||
var _busy: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_configure_style()
|
||||
_saves_tab.pressed.connect(_select_page.bind(PAGE_SAVES, true))
|
||||
_new_slot_tab.pressed.connect(_open_fresh_slot_page)
|
||||
_play_button.pressed.connect(_request_play)
|
||||
_rename_button.pressed.connect(_rename_selected_slot)
|
||||
_duplicate_button.pressed.connect(_prepare_duplicate)
|
||||
_export_button.pressed.connect(_choose_export_path)
|
||||
_delete_button.pressed.connect(_confirm_delete_selected_slot)
|
||||
_import_button.pressed.connect(_choose_import_path)
|
||||
_generated_button.pressed.connect(
|
||||
_set_world_layout.bind(WorldLayoutType.GENERATED)
|
||||
)
|
||||
_starter_button.pressed.connect(
|
||||
_set_world_layout.bind(WorldLayoutType.STARTER_ISLAND)
|
||||
)
|
||||
_random_seed_button.pressed.connect(_choose_random_seed)
|
||||
_custom_seed_button.pressed.connect(_choose_custom_seed)
|
||||
_new_slot_name.text_changed.connect(_on_new_slot_name_changed)
|
||||
_seed_edit.text_changed.connect(_on_seed_text_changed)
|
||||
_seed_edit.text_submitted.connect(_on_seed_submitted)
|
||||
_new_slot_name.text_submitted.connect(_on_new_slot_name_submitted)
|
||||
_create_button.pressed.connect(_request_create)
|
||||
_back_button.pressed.connect(request_back)
|
||||
visibility_changed.connect(_on_visibility_changed)
|
||||
hide()
|
||||
|
||||
|
||||
func _configure_style() -> void:
|
||||
UtilityPageStyle.apply_page(self)
|
||||
_main_panel.add_theme_stylebox_override(
|
||||
"panel",
|
||||
UtilityPageStyle.rounded_style(
|
||||
UtilityPageStyle.OCEAN_PANEL_MID,
|
||||
28,
|
||||
),
|
||||
)
|
||||
_content_panel.add_theme_stylebox_override(
|
||||
"panel",
|
||||
UtilityPageStyle.rounded_style(
|
||||
UtilityPageStyle.OCEAN_FIELD,
|
||||
20,
|
||||
),
|
||||
)
|
||||
for node: Node in find_children("*", "Label", true, false):
|
||||
var label := node as Label
|
||||
label.add_theme_color_override(
|
||||
"font_color",
|
||||
UtilityPageStyle.OCEAN_TEXT_PRIMARY,
|
||||
)
|
||||
for node: Node in find_children("*", "Button", true, false):
|
||||
if node is OrganizerTab:
|
||||
continue
|
||||
UtilityPageStyle.apply_ocean_button(node as BaseButton)
|
||||
_delete_button.add_theme_stylebox_override(
|
||||
"normal",
|
||||
UtilityPageStyle.ocean_button_style(
|
||||
UtilityPageStyle.OCEAN_DANGER,
|
||||
),
|
||||
)
|
||||
_back_button.add_theme_stylebox_override(
|
||||
"normal",
|
||||
UtilityPageStyle.ocean_button_style(
|
||||
UtilityPageStyle.OCEAN_PANEL_DEEP,
|
||||
),
|
||||
)
|
||||
_status.add_theme_color_override(
|
||||
"font_color",
|
||||
UtilityPageStyle.OCEAN_TEXT_SECONDARY,
|
||||
)
|
||||
|
||||
|
||||
func setup(
|
||||
catalog: PlayerSaveSlotCatalog,
|
||||
data_root: PlayerDataRoot,
|
||||
interface_fonts: InterfaceFontController,
|
||||
) -> void:
|
||||
_catalog = catalog
|
||||
_data_root = data_root
|
||||
_interface_fonts = interface_fonts
|
||||
if not _catalog.slots_changed.is_connected(_on_slots_changed):
|
||||
_catalog.slots_changed.connect(_on_slots_changed)
|
||||
|
||||
|
||||
func open_page() -> void:
|
||||
_busy = false
|
||||
_status.text = ""
|
||||
_duplicate_source_slot_id = ""
|
||||
_refresh_slots()
|
||||
if _catalog != null and _catalog.has_slots():
|
||||
_select_page(PAGE_SAVES, false)
|
||||
else:
|
||||
_prepare_fresh_slot()
|
||||
_select_page(PAGE_NEW, false)
|
||||
show()
|
||||
_main_panel.modulate.a = 1.0
|
||||
_main_panel.scale = Vector2.ONE
|
||||
UtilityPageStyle.animate_in(self)
|
||||
call_deferred("_focus_open_page")
|
||||
|
||||
|
||||
func close_page() -> void:
|
||||
_release_owned_focus()
|
||||
hide()
|
||||
|
||||
|
||||
func request_back() -> void:
|
||||
if _import_dialog != null and _import_dialog.visible:
|
||||
_import_dialog.hide()
|
||||
return
|
||||
if _export_dialog != null and _export_dialog.visible:
|
||||
_export_dialog.hide()
|
||||
return
|
||||
if _delete_dialog != null and _delete_dialog.visible:
|
||||
_delete_dialog.hide()
|
||||
return
|
||||
if _busy:
|
||||
return
|
||||
if _active_page_id == PAGE_NEW and _catalog != null and _catalog.has_slots():
|
||||
_duplicate_source_slot_id = ""
|
||||
_select_page(PAGE_SAVES, true)
|
||||
return
|
||||
back_requested.emit()
|
||||
|
||||
|
||||
func set_status(message: String) -> void:
|
||||
_busy = false
|
||||
_status.text = message
|
||||
_refresh_action_state()
|
||||
|
||||
|
||||
func finish_request_if_pending(message: String) -> void:
|
||||
if _busy:
|
||||
set_status(message)
|
||||
|
||||
|
||||
func refresh_page() -> void:
|
||||
if visible:
|
||||
_refresh_slots()
|
||||
|
||||
|
||||
func get_active_page_id() -> StringName:
|
||||
return _active_page_id if visible else StringName()
|
||||
|
||||
|
||||
func _select_page(page_id: StringName, focus_tab: bool) -> void:
|
||||
if page_id not in [PAGE_SAVES, PAGE_NEW]:
|
||||
return
|
||||
_active_page_id = page_id
|
||||
_saves_page.visible = page_id == PAGE_SAVES
|
||||
_new_slot_page.visible = page_id == PAGE_NEW
|
||||
_saves_tab.set_selected(page_id == PAGE_SAVES, is_inside_tree())
|
||||
_new_slot_tab.set_selected(page_id == PAGE_NEW, is_inside_tree())
|
||||
_configure_controller_focus()
|
||||
if focus_tab:
|
||||
(_saves_tab if page_id == PAGE_SAVES else _new_slot_tab).grab_focus()
|
||||
|
||||
|
||||
func _open_fresh_slot_page() -> void:
|
||||
_prepare_fresh_slot()
|
||||
_select_page(PAGE_NEW, true)
|
||||
|
||||
|
||||
func _prepare_fresh_slot() -> void:
|
||||
_duplicate_source_slot_id = ""
|
||||
_new_slot_heading.text = "new progression"
|
||||
_new_slot_name.text = _suggested_slot_name()
|
||||
_world_layout = WorldLayoutType.GENERATED
|
||||
_seed_mode = SeedMode.RANDOM
|
||||
_roll_random_seed()
|
||||
_refresh_new_slot_presentation()
|
||||
|
||||
|
||||
func _prepare_duplicate() -> void:
|
||||
var slot: Dictionary = _selected_slot()
|
||||
if slot.is_empty() or not bool(slot.get("has_save", false)):
|
||||
return
|
||||
_duplicate_source_slot_id = _selected_slot_id
|
||||
_new_slot_heading.text = "duplicate progression"
|
||||
_new_slot_name.text = PlayerSaveSlotCatalog.normalized_name(
|
||||
"copy of %s" % str(slot.get("display_name", "save"))
|
||||
)
|
||||
_world_layout = WorldLayoutType.GENERATED
|
||||
_seed_mode = SeedMode.RANDOM
|
||||
_roll_random_seed()
|
||||
_refresh_new_slot_presentation()
|
||||
_select_page(PAGE_NEW, true)
|
||||
|
||||
|
||||
func _refresh_slots() -> void:
|
||||
for button: Button in _slot_buttons:
|
||||
_slot_list.remove_child(button)
|
||||
button.queue_free()
|
||||
_slot_buttons.clear()
|
||||
if _catalog == null:
|
||||
_selected_slot_id = ""
|
||||
_empty_slots_label.show()
|
||||
_refresh_selected_slot()
|
||||
return
|
||||
var slots: Array[Dictionary] = _catalog.list_slots()
|
||||
var known_selection: bool = false
|
||||
for slot: Dictionary in slots:
|
||||
var slot_id: String = str(slot.get("slot_id", ""))
|
||||
var button := Button.new()
|
||||
button.name = "Slot_%s" % slot_id.left(8)
|
||||
button.custom_minimum_size = Vector2(0.0, 56.0)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
button.toggle_mode = true
|
||||
button.text = str(slot.get("display_name", "save")) + (
|
||||
" · active" if bool(slot.get("active", false)) else ""
|
||||
)
|
||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
UtilityPageStyle.apply_ocean_button(button)
|
||||
button.pressed.connect(_select_slot.bind(slot_id, false))
|
||||
button.focus_entered.connect(_select_slot.bind(slot_id, true))
|
||||
_slot_list.add_child(button)
|
||||
_slot_buttons.append(button)
|
||||
known_selection = known_selection or slot_id == _selected_slot_id
|
||||
_empty_slots_label.visible = slots.is_empty()
|
||||
if not known_selection:
|
||||
_selected_slot_id = _catalog.get_active_slot_id()
|
||||
if _selected_slot_id.is_empty() and not slots.is_empty():
|
||||
_selected_slot_id = str(slots.front().get("slot_id", ""))
|
||||
_refresh_selected_slot()
|
||||
_configure_controller_focus()
|
||||
|
||||
|
||||
func _select_slot(slot_id: String, _from_focus: bool) -> void:
|
||||
if _catalog == null or _catalog.get_slot(slot_id).is_empty():
|
||||
return
|
||||
_selected_slot_id = slot_id
|
||||
_refresh_selected_slot()
|
||||
|
||||
|
||||
func _refresh_selected_slot() -> void:
|
||||
var slot: Dictionary = _selected_slot()
|
||||
for button: Button in _slot_buttons:
|
||||
button.set_pressed_no_signal(
|
||||
button.name == "Slot_%s" % _selected_slot_id.left(8)
|
||||
)
|
||||
if slot.is_empty():
|
||||
_slot_name_edit.text = ""
|
||||
_slot_name_edit.editable = false
|
||||
_slot_summary.text = "[center]create or import a save slot to begin.[/center]"
|
||||
else:
|
||||
_slot_name_edit.editable = true
|
||||
_slot_name_edit.text = str(slot.get("display_name", "save"))
|
||||
_slot_summary.text = _format_slot_summary(slot)
|
||||
_refresh_action_state()
|
||||
|
||||
|
||||
func _format_slot_summary(slot: Dictionary) -> String:
|
||||
if not bool(slot.get("has_save", false)):
|
||||
return "[center]%s[/center]" % str(
|
||||
slot.get("status_message", "this slot has no progression yet.")
|
||||
)
|
||||
var last_played: String = _format_timestamp(
|
||||
int(slot.get("last_played_at_unix", 0))
|
||||
)
|
||||
return (
|
||||
"[font_size=24]level %d[/font_size]\n\n"
|
||||
+ "%d catches · %d discovered\n"
|
||||
+ "%d fishcoins\n\n"
|
||||
+ "%s · seed %d\n"
|
||||
+ "last played %s"
|
||||
) % [
|
||||
int(slot.get("player_level", 1)),
|
||||
int(slot.get("catch_count", 0)),
|
||||
int(slot.get("discovered_species_count", 0)),
|
||||
int(slot.get("wallet_balance", 0)),
|
||||
WorldLayoutType.display_name(slot.get("world_layout", "")),
|
||||
int(slot.get("world_seed", 0)),
|
||||
last_played,
|
||||
]
|
||||
|
||||
|
||||
func _refresh_action_state() -> void:
|
||||
var slot: Dictionary = _selected_slot()
|
||||
var has_slot: bool = not slot.is_empty()
|
||||
var has_save: bool = has_slot and bool(slot.get("has_save", false))
|
||||
_play_button.disabled = _busy or not has_save
|
||||
_rename_button.disabled = _busy or not has_slot
|
||||
_duplicate_button.disabled = _busy or not has_save
|
||||
_export_button.disabled = _busy or not has_save
|
||||
_delete_button.disabled = _busy or not has_slot
|
||||
_import_button.disabled = _busy or _catalog == null
|
||||
_create_button.disabled = _busy or not _new_slot_request_is_valid()
|
||||
|
||||
|
||||
func _request_play() -> void:
|
||||
if _play_button.disabled or _selected_slot_id.is_empty():
|
||||
return
|
||||
_busy = true
|
||||
_status.text = "loading save slot..."
|
||||
_refresh_action_state()
|
||||
play_requested.emit(_selected_slot_id)
|
||||
|
||||
|
||||
func _rename_selected_slot() -> void:
|
||||
if _catalog == null or _rename_button.disabled:
|
||||
return
|
||||
if not _catalog.rename_slot(_selected_slot_id, _slot_name_edit.text):
|
||||
_status.text = "the save slot could not be renamed."
|
||||
return
|
||||
_status.text = "save slot renamed."
|
||||
|
||||
|
||||
func _confirm_delete_selected_slot() -> void:
|
||||
var slot: Dictionary = _selected_slot()
|
||||
if slot.is_empty() or _delete_button.disabled:
|
||||
return
|
||||
if _delete_dialog == null:
|
||||
_delete_dialog = ConfirmationDialog.new()
|
||||
_delete_dialog.title = "delete save slot?"
|
||||
_delete_dialog.ok_button_text = "delete"
|
||||
_delete_dialog.confirmed.connect(_delete_selected_slot)
|
||||
if _interface_fonts != null:
|
||||
_interface_fonts.apply_utility_theme(_delete_dialog)
|
||||
add_child(_delete_dialog)
|
||||
_delete_dialog.dialog_text = (
|
||||
"delete \"%s\"? this cannot be undone from the game."
|
||||
% str(slot.get("display_name", "save"))
|
||||
)
|
||||
_delete_dialog.popup_centered(Vector2i(560, 260))
|
||||
_configure_delete_dialog.call_deferred()
|
||||
|
||||
|
||||
func _configure_delete_dialog() -> void:
|
||||
if (
|
||||
_delete_dialog != null
|
||||
and is_instance_valid(_delete_dialog)
|
||||
and _delete_dialog.visible
|
||||
):
|
||||
DialogControllerNavigationType.configure_scope(
|
||||
_delete_dialog,
|
||||
_delete_dialog.get_cancel_button(),
|
||||
)
|
||||
|
||||
|
||||
func _delete_selected_slot() -> void:
|
||||
if _catalog == null or _selected_slot_id.is_empty():
|
||||
return
|
||||
if not _catalog.delete_slot(_selected_slot_id):
|
||||
_status.text = "the save slot could not be deleted."
|
||||
return
|
||||
_selected_slot_id = _catalog.get_active_slot_id()
|
||||
_status.text = "save slot deleted."
|
||||
_refresh_slots()
|
||||
if not _catalog.has_slots():
|
||||
_prepare_fresh_slot()
|
||||
_select_page(PAGE_NEW, true)
|
||||
|
||||
|
||||
func _choose_import_path() -> void:
|
||||
if _catalog == null or _data_root == null:
|
||||
return
|
||||
if _import_dialog == null:
|
||||
_import_dialog = FileDialog.new()
|
||||
_import_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
_import_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
_import_dialog.use_native_dialog = false
|
||||
_import_dialog.filters = PackedStringArray([
|
||||
"*.nfsave ; NETfishing progression archive",
|
||||
])
|
||||
_import_dialog.file_selected.connect(_import_file_selected)
|
||||
if _interface_fonts != null:
|
||||
_interface_fonts.apply_utility_theme(_import_dialog)
|
||||
add_child(_import_dialog)
|
||||
_import_dialog.current_dir = _data_root.progression_backup_directory()
|
||||
if _interface_fonts != null:
|
||||
_interface_fonts.popup_file_dialog(_import_dialog)
|
||||
else:
|
||||
_import_dialog.popup_centered_ratio(0.85)
|
||||
|
||||
|
||||
func _import_file_selected(path: String) -> void:
|
||||
var suggested_name: String = path.get_file().get_basename().replace("_", " ")
|
||||
suggested_name = suggested_name.replace("-", " ")
|
||||
var result: Dictionary = _catalog.import_slot(path, suggested_name)
|
||||
_status.text = str(result.get("message", "progression import failed."))
|
||||
if bool(result.get("ok", false)):
|
||||
_selected_slot_id = str(result.get("slot_id", ""))
|
||||
_refresh_slots()
|
||||
_select_page(PAGE_SAVES, false)
|
||||
|
||||
|
||||
func _choose_export_path() -> void:
|
||||
var slot: Dictionary = _selected_slot()
|
||||
if slot.is_empty() or _data_root == null:
|
||||
return
|
||||
var timestamp: String = Time.get_datetime_string_from_system().replace(":", "-")
|
||||
var filename: String = "%s-%s%s" % [
|
||||
_safe_filename(str(slot.get("display_name", "save"))),
|
||||
timestamp,
|
||||
PlayerSaveManager.ARCHIVE_EXTENSION,
|
||||
]
|
||||
if _export_dialog == null:
|
||||
_export_dialog = FileDialog.new()
|
||||
_export_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
|
||||
_export_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
_export_dialog.use_native_dialog = false
|
||||
_export_dialog.filters = PackedStringArray([
|
||||
"*.nfsave ; NETfishing progression archive",
|
||||
])
|
||||
_export_dialog.file_selected.connect(_export_file_selected)
|
||||
if _interface_fonts != null:
|
||||
_interface_fonts.apply_utility_theme(_export_dialog)
|
||||
add_child(_export_dialog)
|
||||
_export_dialog.current_dir = _data_root.progression_backup_directory()
|
||||
_export_dialog.current_file = filename
|
||||
if _interface_fonts != null:
|
||||
_interface_fonts.popup_file_dialog(_export_dialog)
|
||||
else:
|
||||
_export_dialog.popup_centered_ratio(0.85)
|
||||
|
||||
|
||||
func _export_file_selected(path: String) -> void:
|
||||
var destination: String = (
|
||||
path
|
||||
if path.ends_with(PlayerSaveManager.ARCHIVE_EXTENSION)
|
||||
else path + PlayerSaveManager.ARCHIVE_EXTENSION
|
||||
)
|
||||
var result: Dictionary = _catalog.export_slot(
|
||||
_selected_slot_id,
|
||||
destination,
|
||||
)
|
||||
_status.text = str(result.get("message", "progression export failed."))
|
||||
|
||||
|
||||
func _request_create() -> void:
|
||||
if _create_button.disabled:
|
||||
return
|
||||
var seed: int = _selected_world_seed()
|
||||
if _world_layout == WorldLayoutType.GENERATED and seed == 0:
|
||||
_status.text = "enter text or a whole number from 1 to %d." % SaveManagerType.MAX_WORLD_SEED
|
||||
_seed_edit.grab_focus()
|
||||
_seed_edit.select_all()
|
||||
return
|
||||
if seed == 0:
|
||||
seed = SaveManagerType.roll_world_seed()
|
||||
_busy = true
|
||||
_status.text = "creating save slot..."
|
||||
_refresh_action_state()
|
||||
create_requested.emit(
|
||||
PlayerSaveSlotCatalog.normalized_name(_new_slot_name.text),
|
||||
_world_layout,
|
||||
seed,
|
||||
_duplicate_source_slot_id,
|
||||
)
|
||||
|
||||
|
||||
func _set_world_layout(layout: StringName) -> void:
|
||||
if not WorldLayoutType.is_valid(layout):
|
||||
return
|
||||
_world_layout = layout
|
||||
_status.text = ""
|
||||
_refresh_new_slot_presentation()
|
||||
|
||||
|
||||
func _choose_random_seed() -> void:
|
||||
_seed_mode = SeedMode.RANDOM
|
||||
_roll_random_seed()
|
||||
_status.text = ""
|
||||
_refresh_new_slot_presentation()
|
||||
|
||||
|
||||
func _choose_custom_seed() -> void:
|
||||
_seed_mode = SeedMode.CUSTOM
|
||||
if NewGameSetupPageType.parse_seed_text(_seed_edit.text) == _random_seed:
|
||||
_seed_edit.text = ""
|
||||
_status.text = ""
|
||||
_refresh_new_slot_presentation()
|
||||
_seed_edit.grab_focus.call_deferred()
|
||||
_seed_edit.select_all.call_deferred()
|
||||
|
||||
|
||||
func _roll_random_seed() -> void:
|
||||
_random_seed = SaveManagerType.roll_world_seed()
|
||||
_seed_edit.text = str(_random_seed)
|
||||
|
||||
|
||||
func _selected_world_seed() -> int:
|
||||
return (
|
||||
_random_seed
|
||||
if _seed_mode == SeedMode.RANDOM
|
||||
else NewGameSetupPageType.parse_seed_text(_seed_edit.text)
|
||||
)
|
||||
|
||||
|
||||
func _on_seed_text_changed(_text: String) -> void:
|
||||
if _seed_mode == SeedMode.CUSTOM:
|
||||
_status.text = ""
|
||||
_refresh_action_state()
|
||||
|
||||
|
||||
func _on_new_slot_name_changed(_text: String) -> void:
|
||||
_refresh_action_state()
|
||||
|
||||
|
||||
func _on_seed_submitted(_text: String) -> void:
|
||||
_request_create()
|
||||
|
||||
|
||||
func _on_new_slot_name_submitted(_text: String) -> void:
|
||||
if _world_layout == WorldLayoutType.STARTER_ISLAND:
|
||||
_request_create()
|
||||
|
||||
|
||||
func _refresh_new_slot_presentation() -> void:
|
||||
var generated: bool = _world_layout == WorldLayoutType.GENERATED
|
||||
_generated_button.set_pressed_no_signal(generated)
|
||||
_starter_button.set_pressed_no_signal(not generated)
|
||||
_world_description.text = (
|
||||
"build a new island from terrain chunks.\n"
|
||||
+ "the same seed always builds the same world."
|
||||
if generated
|
||||
else "play on the starter island."
|
||||
)
|
||||
_seed_section.visible = generated
|
||||
_random_seed_button.set_pressed_no_signal(_seed_mode == SeedMode.RANDOM)
|
||||
_custom_seed_button.set_pressed_no_signal(_seed_mode == SeedMode.CUSTOM)
|
||||
_seed_edit.editable = _seed_mode == SeedMode.CUSTOM
|
||||
_seed_edit.focus_mode = (
|
||||
Control.FOCUS_ALL if _seed_mode == SeedMode.CUSTOM else Control.FOCUS_NONE
|
||||
)
|
||||
_seed_help.text = (
|
||||
"press random seed again to roll another world."
|
||||
if _seed_mode == SeedMode.RANDOM
|
||||
else "enter text or a number from 1 to %d."
|
||||
% SaveManagerType.MAX_WORLD_SEED
|
||||
)
|
||||
_create_button.text = (
|
||||
"duplicate and play"
|
||||
if not _duplicate_source_slot_id.is_empty()
|
||||
else "create and play"
|
||||
)
|
||||
_refresh_action_state()
|
||||
_configure_controller_focus()
|
||||
|
||||
|
||||
func _new_slot_request_is_valid() -> bool:
|
||||
return (
|
||||
not PlayerSaveSlotCatalog.normalized_name(_new_slot_name.text).is_empty()
|
||||
and (
|
||||
_world_layout != WorldLayoutType.GENERATED
|
||||
or _seed_mode == SeedMode.RANDOM
|
||||
or NewGameSetupPageType.parse_seed_text(_seed_edit.text) > 0
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _configure_controller_focus() -> void:
|
||||
if not is_node_ready():
|
||||
return
|
||||
_saves_tab.focus_neighbor_right = _saves_tab.get_path_to(_new_slot_tab)
|
||||
_new_slot_tab.focus_neighbor_left = _new_slot_tab.get_path_to(_saves_tab)
|
||||
if _active_page_id == PAGE_SAVES:
|
||||
var first: Control = _slot_buttons.front() if not _slot_buttons.is_empty() else _import_button
|
||||
var last: Control = _slot_buttons.back() if not _slot_buttons.is_empty() else _import_button
|
||||
_saves_tab.focus_neighbor_bottom = _saves_tab.get_path_to(first)
|
||||
_new_slot_tab.focus_neighbor_bottom = _new_slot_tab.get_path_to(first)
|
||||
for index: int in _slot_buttons.size():
|
||||
var button: Button = _slot_buttons[index]
|
||||
var top: Control = _saves_tab if index == 0 else _slot_buttons[index - 1]
|
||||
var bottom: Control = _import_button if index == _slot_buttons.size() - 1 else _slot_buttons[index + 1]
|
||||
_set_neighbors(button, button, _slot_name_edit, top, bottom)
|
||||
_set_neighbors(_import_button, _import_button, _slot_name_edit, last, _back_button)
|
||||
_set_neighbors(_slot_name_edit, first, _slot_name_edit, _saves_tab, _play_button)
|
||||
_set_neighbors(_play_button, _import_button, _rename_button, _slot_name_edit, _duplicate_button)
|
||||
_set_neighbors(_rename_button, _play_button, _rename_button, _slot_name_edit, _export_button)
|
||||
_set_neighbors(_duplicate_button, _import_button, _export_button, _play_button, _delete_button)
|
||||
_set_neighbors(_export_button, _duplicate_button, _export_button, _rename_button, _delete_button)
|
||||
_set_neighbors(_delete_button, _import_button, _delete_button, _duplicate_button, _back_button)
|
||||
_set_neighbors(_back_button, _back_button, _back_button, _import_button, _back_button)
|
||||
return
|
||||
_saves_tab.focus_neighbor_bottom = _saves_tab.get_path_to(_new_slot_name)
|
||||
_new_slot_tab.focus_neighbor_bottom = _new_slot_tab.get_path_to(_new_slot_name)
|
||||
var generated: bool = _world_layout == WorldLayoutType.GENERATED
|
||||
var custom: bool = generated and _seed_mode == SeedMode.CUSTOM
|
||||
_set_neighbors(_new_slot_name, _new_slot_name, _new_slot_name, _new_slot_tab, _generated_button)
|
||||
_set_neighbors(_generated_button, _generated_button, _starter_button, _new_slot_name, _random_seed_button if generated else _create_button)
|
||||
_set_neighbors(_starter_button, _generated_button, _starter_button, _new_slot_name, _custom_seed_button if generated else _back_button)
|
||||
_set_neighbors(_random_seed_button, _random_seed_button, _custom_seed_button, _generated_button, _seed_edit if custom else _create_button)
|
||||
_set_neighbors(_custom_seed_button, _random_seed_button, _custom_seed_button, _starter_button, _seed_edit if custom else _back_button)
|
||||
_set_neighbors(_seed_edit, _seed_edit, _seed_edit, _custom_seed_button, _create_button)
|
||||
var action_top: Control = _seed_edit if custom else _random_seed_button if generated else _generated_button
|
||||
_set_neighbors(_create_button, _create_button, _back_button, action_top, _create_button)
|
||||
_set_neighbors(_back_button, _create_button, _back_button, action_top, _back_button)
|
||||
|
||||
|
||||
func _set_neighbors(
|
||||
control: Control,
|
||||
left: Control,
|
||||
right: Control,
|
||||
top: Control,
|
||||
bottom: Control,
|
||||
) -> void:
|
||||
control.focus_neighbor_left = control.get_path_to(left)
|
||||
control.focus_neighbor_right = control.get_path_to(right)
|
||||
control.focus_neighbor_top = control.get_path_to(top)
|
||||
control.focus_neighbor_bottom = control.get_path_to(bottom)
|
||||
|
||||
|
||||
func _focus_open_page() -> void:
|
||||
if not visible:
|
||||
return
|
||||
(_saves_tab if _active_page_id == PAGE_SAVES else _new_slot_tab).grab_focus()
|
||||
|
||||
|
||||
func _selected_slot() -> Dictionary:
|
||||
return _catalog.get_slot(_selected_slot_id) if _catalog != null else {}
|
||||
|
||||
|
||||
func _suggested_slot_name() -> String:
|
||||
var used: Dictionary[String, bool] = {}
|
||||
if _catalog != null:
|
||||
for slot: Dictionary in _catalog.list_slots():
|
||||
used[str(slot.get("display_name", "")).to_lower()] = true
|
||||
var number: int = 1
|
||||
while used.has("save %d" % number):
|
||||
number += 1
|
||||
return "save %d" % number
|
||||
|
||||
|
||||
func _format_timestamp(unix_time: int) -> String:
|
||||
if unix_time <= 0:
|
||||
return "never"
|
||||
var value: Dictionary = Time.get_datetime_dict_from_unix_time(unix_time)
|
||||
return "%04d-%02d-%02d" % [
|
||||
int(value.get("year", 0)),
|
||||
int(value.get("month", 0)),
|
||||
int(value.get("day", 0)),
|
||||
]
|
||||
|
||||
|
||||
func _safe_filename(value: String) -> String:
|
||||
var result: String = value.strip_edges().to_lower().replace(" ", "-")
|
||||
for character: String in ["/", "\\", ":", "*", "?", "\"", "<", ">", "|"]:
|
||||
result = result.replace(character, "")
|
||||
return "netfishing-save" if result.is_empty() else result
|
||||
|
||||
|
||||
func _on_slots_changed() -> void:
|
||||
if visible:
|
||||
_refresh_slots()
|
||||
|
||||
|
||||
func _on_visibility_changed() -> void:
|
||||
if not visible:
|
||||
_release_owned_focus()
|
||||
|
||||
|
||||
func _release_owned_focus() -> void:
|
||||
if not is_inside_tree():
|
||||
return
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if focus_owner != null and is_ancestor_of(focus_owner):
|
||||
focus_owner.release_focus()
|
||||
1
ui/save_slots_page.gd.uid
Normal file
1
ui/save_slots_page.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cytwdvxf22106
|
||||
350
ui/save_slots_page.tscn
Normal file
350
ui/save_slots_page.tscn
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/save_slots_page.gd" id="1_script"]
|
||||
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
||||
[ext_resource type="Script" path="res://ui/components/organizer_tab.gd" id="3_tab"]
|
||||
|
||||
[node name="SaveSlotsPage" type="Control"]
|
||||
visible = false
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme = ExtResource("2_theme")
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="MainPanel" type="PanelContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -440.0
|
||||
offset_top = -330.0
|
||||
offset_right = 440.0
|
||||
offset_bottom = 330.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="OuterMargin" type="MarginContainer" parent="MainPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 24
|
||||
theme_override_constants/margin_top = 18
|
||||
theme_override_constants/margin_right = 24
|
||||
theme_override_constants/margin_bottom = 18
|
||||
|
||||
[node name="Layout" type="VBoxContainer" parent="MainPanel/OuterMargin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = -26
|
||||
|
||||
[node name="Heading" type="Label" parent="MainPanel/OuterMargin/Layout"]
|
||||
custom_minimum_size = Vector2(0, 66)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 30
|
||||
text = "play"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="TabBar" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout"]
|
||||
custom_minimum_size = Vector2(0, 52)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
alignment = 1
|
||||
|
||||
[node name="SavesTab" type="Button" parent="MainPanel/OuterMargin/Layout/TabBar"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(180, 52)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "save slots"
|
||||
script = ExtResource("3_tab")
|
||||
|
||||
[node name="NewSlotTab" type="Button" parent="MainPanel/OuterMargin/Layout/TabBar"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(180, 52)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "new slot"
|
||||
script = ExtResource("3_tab")
|
||||
palette_index = 1
|
||||
|
||||
[node name="ContentPanel" type="PanelContainer" parent="MainPanel/OuterMargin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 474)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ContentMargin" type="MarginContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 28
|
||||
theme_override_constants/margin_top = 20
|
||||
theme_override_constants/margin_right = 28
|
||||
theme_override_constants/margin_bottom = 20
|
||||
|
||||
[node name="PageStack" type="Control" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SavesPage" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="SlotsColumn" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage"]
|
||||
custom_minimum_size = Vector2(340, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Title" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/SlotsColumn"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "save slots"
|
||||
|
||||
[node name="SlotScroll" type="ScrollContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/SlotsColumn"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
follow_focus = true
|
||||
|
||||
[node name="SlotList" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/SlotsColumn/SlotScroll"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="EmptySlotsLabel" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/SlotsColumn/SlotScroll/SlotList"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 100)
|
||||
layout_mode = 2
|
||||
text = "no save slots yet"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="ImportSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/SlotsColumn"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "import save"
|
||||
|
||||
[node name="Divider" type="VSeparator" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="DetailsColumn" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Title" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "selected slot"
|
||||
|
||||
[node name="SelectedSlotName" type="LineEdit" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
placeholder_text = "save slot name"
|
||||
max_length = 32
|
||||
|
||||
[node name="SlotSummary" type="RichTextLabel" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
bbcode_enabled = true
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
autowrap_mode = 2
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="Actions" type="GridContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/h_separation = 10
|
||||
theme_override_constants/v_separation = 10
|
||||
columns = 2
|
||||
|
||||
[node name="PlaySlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
focus_mode = 2
|
||||
text = "play"
|
||||
|
||||
[node name="RenameSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
focus_mode = 2
|
||||
text = "rename"
|
||||
|
||||
[node name="DuplicateSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
focus_mode = 2
|
||||
text = "duplicate"
|
||||
|
||||
[node name="ExportSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
focus_mode = 2
|
||||
text = "export"
|
||||
|
||||
[node name="DeleteSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn/Actions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
focus_mode = 2
|
||||
text = "delete"
|
||||
|
||||
[node name="NewSlotPage" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 9
|
||||
|
||||
[node name="NewSlotHeading" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "new progression"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="NewSlotName" type="LineEdit" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
placeholder_text = "save slot name"
|
||||
alignment = 1
|
||||
max_length = 32
|
||||
|
||||
[node name="WorldButtons" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
alignment = 1
|
||||
|
||||
[node name="GeneratedButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage/WorldButtons"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(230, 48)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
toggle_mode = true
|
||||
text = "generate a world"
|
||||
|
||||
[node name="StarterButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage/WorldButtons"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(230, 48)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
toggle_mode = true
|
||||
text = "starter island"
|
||||
|
||||
[node name="WorldDescription" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 42)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 15
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="SeedSection" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 7
|
||||
|
||||
[node name="SeedModes" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage/SeedSection"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
alignment = 1
|
||||
|
||||
[node name="RandomSeedButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage/SeedSection/SeedModes"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(190, 44)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
toggle_mode = true
|
||||
text = "random seed"
|
||||
|
||||
[node name="CustomSeedButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage/SeedSection/SeedModes"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(190, 44)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
toggle_mode = true
|
||||
text = "enter a seed"
|
||||
|
||||
[node name="SeedEdit" type="LineEdit" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage/SeedSection"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 42)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
placeholder_text = "number or words"
|
||||
alignment = 1
|
||||
max_length = 64
|
||||
|
||||
[node name="SeedHelp" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage/SeedSection"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 24)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Spacer" type="Control" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="CreateSlotButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/NewSlotPage"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(260, 48)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
focus_mode = 2
|
||||
text = "create and play"
|
||||
|
||||
[node name="FooterGroup" type="MarginContainer" parent="MainPanel/OuterMargin/Layout"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_top = 34
|
||||
|
||||
[node name="FooterLayout" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/FooterGroup"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="Status" type="Label" parent="MainPanel/OuterMargin/Layout/FooterGroup/FooterLayout"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 24)
|
||||
layout_mode = 2
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="Footer" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/FooterGroup/FooterLayout"]
|
||||
layout_mode = 2
|
||||
alignment = 2
|
||||
|
||||
[node name="BackButton" type="Button" parent="MainPanel/OuterMargin/Layout/FooterGroup/FooterLayout/Footer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(150, 46)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
text = "back"
|
||||
|
|
@ -86,6 +86,7 @@ enum PresentationMode {
|
|||
%ControllerSensitivityValue
|
||||
)
|
||||
@onready var _invert_y_toggle: Button = %InvertYToggle
|
||||
@onready var _swap_scroll_toggle: Button = %SwapScrollToggle
|
||||
@onready var _on_screen_keyboard_toggle: Button = %OnScreenKeyboardToggle
|
||||
@onready var _auto_click_toggle: Button = %AutoClickToggle
|
||||
@onready var _auto_click_interval_slider: HSlider = (
|
||||
|
|
@ -112,6 +113,7 @@ var _auto_click_interval_value: float = 0.20
|
|||
var _mouse_sensitivity: float = 0.005
|
||||
var _controller_sensitivity: float = 2.5
|
||||
var _invert_camera_y: bool = false
|
||||
var _swap_hotbar_camera_scroll: bool = false
|
||||
var _on_screen_keyboard_enabled: bool = false
|
||||
var _chat_dock_right: bool = false
|
||||
var _chat_mobile_mode: bool = false
|
||||
|
|
@ -125,7 +127,6 @@ var _environment_volume: float = 1.0
|
|||
var _network_profile: NetworkProfilePreferences
|
||||
var _network_session: NetworkSession
|
||||
var _data_root: PlayerDataRoot
|
||||
var _progression_saves: PlayerSaveManager
|
||||
var _identity_backups: IdentityBackupService
|
||||
var _player_identity: PlayerIdentityStore
|
||||
var _host_identity: HostIdentityStore
|
||||
|
|
@ -135,8 +136,6 @@ var _controller_mapping_panel: ControllerMappingPanelType
|
|||
var _keyboard_mouse_mapping_manager: KeyboardMouseMappingManagerType
|
||||
var _keyboard_mouse_mapping_panel: KeyboardMouseMappingPanelType
|
||||
var _data_folder_dialog: FileDialog
|
||||
var _progression_import_dialog: FileDialog
|
||||
var _progression_export_dialog: FileDialog
|
||||
var _backup_file_dialog: FileDialog
|
||||
var _export_file_dialog: FileDialog
|
||||
var _passphrase_dialog: ConfirmationDialog
|
||||
|
|
@ -146,7 +145,6 @@ var _pending_identity_operation := ""
|
|||
var _pending_identity_type := ""
|
||||
var _pending_identity_path := ""
|
||||
var _pending_import_data: Dictionary = {}
|
||||
var _pending_progression_path := ""
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
|
|
@ -239,6 +237,7 @@ func _connect_controls() -> void:
|
|||
_on_controller_sensitivity_changed
|
||||
)
|
||||
_invert_y_toggle.toggled.connect(_set_invert_y)
|
||||
_swap_scroll_toggle.toggled.connect(_set_swap_scroll)
|
||||
_on_screen_keyboard_toggle.toggled.connect(_set_on_screen_keyboard)
|
||||
_auto_click_toggle.toggled.connect(_set_auto_click)
|
||||
_auto_click_interval_slider.value_changed.connect(
|
||||
|
|
@ -248,8 +247,6 @@ func _connect_controls() -> void:
|
|||
%KeyboardMapping.pressed.connect(_open_keyboard_mouse_mapping)
|
||||
%OpenDataFolder.pressed.connect(_open_data_folder)
|
||||
%ChangeDataFolder.pressed.connect(_choose_data_folder)
|
||||
%ExportProgression.pressed.connect(_choose_progression_export)
|
||||
%ImportProgression.pressed.connect(_choose_progression_import)
|
||||
%ExportPlayerIdentity.pressed.connect(
|
||||
_choose_identity_export.bind("player")
|
||||
)
|
||||
|
|
@ -343,7 +340,6 @@ func setup_keyboard_mouse_mapping(
|
|||
|
||||
func setup_data_and_identity(
|
||||
data_root: PlayerDataRoot,
|
||||
progression_saves: PlayerSaveManager,
|
||||
identity_backups: IdentityBackupService,
|
||||
player_identity: PlayerIdentityStore,
|
||||
host_identity: HostIdentityStore,
|
||||
|
|
@ -351,7 +347,6 @@ func setup_data_and_identity(
|
|||
interface_fonts: InterfaceFontController,
|
||||
) -> void:
|
||||
_data_root = data_root
|
||||
_progression_saves = progression_saves
|
||||
_identity_backups = identity_backups
|
||||
_player_identity = player_identity
|
||||
_host_identity = host_identity
|
||||
|
|
@ -619,11 +614,6 @@ func _refresh_data_page() -> void:
|
|||
_data_root.override_active
|
||||
or (_network_session != null and _network_session.is_session_active())
|
||||
)
|
||||
%ExportProgression.disabled = _progression_saves == null
|
||||
%ImportProgression.disabled = (
|
||||
_progression_saves == null
|
||||
or (_network_session != null and _network_session.is_session_active())
|
||||
)
|
||||
var fingerprint: String = (
|
||||
_player_identity.fingerprint if _player_identity != null else ""
|
||||
)
|
||||
|
|
@ -674,122 +664,6 @@ func _copy_player_fingerprint() -> void:
|
|||
_feedback.text = "full player fingerprint copied for server operator setup."
|
||||
|
||||
|
||||
func _choose_progression_export() -> void:
|
||||
if _progression_saves == null or _data_root == null:
|
||||
_feedback.text = "progression export is unavailable."
|
||||
return
|
||||
var timestamp: String = Time.get_datetime_string_from_system().replace(
|
||||
":", "-"
|
||||
)
|
||||
var suggested: String = _data_root.progression_backup_directory().path_join(
|
||||
"NETfishing-progression-%s%s"
|
||||
% [timestamp, PlayerSaveManager.ARCHIVE_EXTENSION]
|
||||
)
|
||||
if _progression_export_dialog == null:
|
||||
_progression_export_dialog = FileDialog.new()
|
||||
_progression_export_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
|
||||
_progression_export_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
_progression_export_dialog.use_native_dialog = false
|
||||
_progression_export_dialog.filters = PackedStringArray([
|
||||
"*.nfsave ; NETfishing progression archive",
|
||||
])
|
||||
_progression_export_dialog.file_selected.connect(
|
||||
_progression_export_file_selected
|
||||
)
|
||||
_interface_fonts.apply_utility_theme(_progression_export_dialog)
|
||||
add_child(_progression_export_dialog)
|
||||
_progression_export_dialog.current_dir = suggested.get_base_dir()
|
||||
_progression_export_dialog.current_file = suggested.get_file()
|
||||
_interface_fonts.popup_file_dialog(_progression_export_dialog)
|
||||
|
||||
|
||||
func _progression_export_file_selected(path: String) -> void:
|
||||
var destination: String = (
|
||||
path
|
||||
if path.ends_with(PlayerSaveManager.ARCHIVE_EXTENSION)
|
||||
else path + PlayerSaveManager.ARCHIVE_EXTENSION
|
||||
)
|
||||
var result: Dictionary = _progression_saves.export_progression_archive(
|
||||
destination
|
||||
)
|
||||
_feedback.text = str(
|
||||
result.get("message", "progression export failed.")
|
||||
)
|
||||
|
||||
|
||||
func _choose_progression_import() -> void:
|
||||
if _progression_saves == null or _data_root == null:
|
||||
_feedback.text = "progression import is unavailable."
|
||||
return
|
||||
if _network_session != null and _network_session.is_session_active():
|
||||
_feedback.text = "return to title before importing progression."
|
||||
return
|
||||
if _progression_import_dialog == null:
|
||||
_progression_import_dialog = FileDialog.new()
|
||||
_progression_import_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
_progression_import_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
_progression_import_dialog.use_native_dialog = false
|
||||
_progression_import_dialog.filters = PackedStringArray([
|
||||
"*.nfsave ; NETfishing progression archive",
|
||||
])
|
||||
_progression_import_dialog.file_selected.connect(
|
||||
_progression_import_file_selected
|
||||
)
|
||||
_interface_fonts.apply_utility_theme(_progression_import_dialog)
|
||||
add_child(_progression_import_dialog)
|
||||
_progression_import_dialog.current_dir = (
|
||||
_data_root.progression_backup_directory()
|
||||
)
|
||||
_interface_fonts.popup_file_dialog(_progression_import_dialog)
|
||||
|
||||
|
||||
func _progression_import_file_selected(path: String) -> void:
|
||||
var inspected: Dictionary = (
|
||||
_progression_saves.inspect_progression_archive(path)
|
||||
)
|
||||
if not bool(inspected.get("ok", false)):
|
||||
_feedback.text = str(
|
||||
inspected.get("message", "progression archive could not be opened.")
|
||||
)
|
||||
return
|
||||
_pending_progression_path = path
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.title = "replace saved progression?"
|
||||
dialog.ok_button_text = "import progression"
|
||||
dialog.dialog_text = (
|
||||
"this will replace the current progression after making a backup.\n\n"
|
||||
+ "fish: %d\ndiscovered: %d\nworld: %s\nworld seed: %d\n\n"
|
||||
+ "identities, settings, friends, bans, and trusted servers are unchanged."
|
||||
) % [
|
||||
int(inspected.get("catch_count", 0)),
|
||||
int(inspected.get("discovered_species_count", 0)),
|
||||
WorldLayout.display_name(inspected.get(
|
||||
"world_layout",
|
||||
String(WorldLayout.GENERATED),
|
||||
)),
|
||||
int(inspected.get("world_seed", 0)),
|
||||
]
|
||||
dialog.confirmed.connect(_confirm_progression_import.bind(dialog))
|
||||
dialog.canceled.connect(dialog.queue_free)
|
||||
_interface_fonts.apply_utility_theme(dialog)
|
||||
add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(640, 390))
|
||||
_configure_confirmation_dialog.call_deferred(
|
||||
dialog, dialog.get_cancel_button()
|
||||
)
|
||||
|
||||
|
||||
func _confirm_progression_import(dialog: ConfirmationDialog) -> void:
|
||||
var result: Dictionary = _progression_saves.import_progression_archive(
|
||||
_pending_progression_path
|
||||
)
|
||||
_feedback.text = str(
|
||||
result.get("message", "progression import failed.")
|
||||
)
|
||||
_pending_progression_path = ""
|
||||
dialog.queue_free()
|
||||
|
||||
|
||||
func _choose_data_folder() -> void:
|
||||
if _data_root == null or _data_root.override_active:
|
||||
_feedback.text = "the data folder is externally managed."
|
||||
|
|
@ -1067,6 +941,7 @@ func _apply_settings() -> void:
|
|||
edited.mouse_camera_sensitivity = _mouse_sensitivity
|
||||
edited.controller_camera_sensitivity = _controller_sensitivity
|
||||
edited.invert_camera_y = _invert_camera_y
|
||||
edited.swap_hotbar_camera_scroll = _swap_hotbar_camera_scroll
|
||||
edited.on_screen_keyboard_enabled = _on_screen_keyboard_enabled
|
||||
edited.chat_dock_right = _chat_dock_right
|
||||
edited.chat_mobile_mode = _chat_mobile_mode
|
||||
|
|
@ -1099,6 +974,7 @@ func _load_controls() -> void:
|
|||
_mouse_sensitivity = settings.mouse_camera_sensitivity
|
||||
_controller_sensitivity = settings.controller_camera_sensitivity
|
||||
_invert_camera_y = settings.invert_camera_y
|
||||
_swap_hotbar_camera_scroll = settings.swap_hotbar_camera_scroll
|
||||
_on_screen_keyboard_enabled = settings.on_screen_keyboard_enabled
|
||||
_chat_dock_right = settings.chat_dock_right
|
||||
_chat_mobile_mode = settings.chat_mobile_mode
|
||||
|
|
@ -1125,6 +1001,7 @@ func _load_controls() -> void:
|
|||
_controller_sensitivity
|
||||
)
|
||||
_invert_y_toggle.set_pressed_no_signal(_invert_camera_y)
|
||||
_swap_scroll_toggle.set_pressed_no_signal(_swap_hotbar_camera_scroll)
|
||||
_on_screen_keyboard_toggle.set_pressed_no_signal(
|
||||
_on_screen_keyboard_enabled
|
||||
)
|
||||
|
|
@ -1142,6 +1019,9 @@ func _refresh_value_labels() -> void:
|
|||
"on" if _fullscreen_toggle.button_pressed else "off"
|
||||
)
|
||||
_invert_y_toggle.text = "on" if _invert_camera_y else "off"
|
||||
_swap_scroll_toggle.text = (
|
||||
"on" if _swap_hotbar_camera_scroll else "off"
|
||||
)
|
||||
_on_screen_keyboard_toggle.text = (
|
||||
"on" if _on_screen_keyboard_enabled else "off"
|
||||
)
|
||||
|
|
@ -1255,6 +1135,11 @@ func _set_invert_y(enabled: bool) -> void:
|
|||
_refresh_value_labels()
|
||||
|
||||
|
||||
func _set_swap_scroll(enabled: bool) -> void:
|
||||
_swap_hotbar_camera_scroll = enabled
|
||||
_refresh_value_labels()
|
||||
|
||||
|
||||
func _set_on_screen_keyboard(enabled: bool) -> void:
|
||||
_on_screen_keyboard_enabled = enabled
|
||||
_refresh_value_labels()
|
||||
|
|
|
|||
|
|
@ -402,12 +402,32 @@ unique_name_in_owner = true
|
|||
custom_minimum_size = Vector2(330, 48)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
focus_neighbor_bottom = NodePath("../SwapScrollToggle")
|
||||
toggle_mode = true
|
||||
text = "off"
|
||||
|
||||
[node name="InvertYSpacer" type="Control" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/ControlsPage/Grid"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SwapScrollLabel" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/ControlsPage/Grid"]
|
||||
custom_minimum_size = Vector2(210, 48)
|
||||
layout_mode = 2
|
||||
text = "swap scroll controls"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="SwapScrollToggle" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/ControlsPage/Grid"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(330, 48)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
focus_neighbor_top = NodePath("../InvertYToggle")
|
||||
focus_neighbor_bottom = NodePath("../OnScreenKeyboardToggle")
|
||||
toggle_mode = true
|
||||
text = "off"
|
||||
|
||||
[node name="SwapScrollSpacer" type="Control" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/ControlsPage/Grid"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="KeyboardLabel" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/ControlsPage/Grid"]
|
||||
custom_minimum_size = Vector2(210, 48)
|
||||
layout_mode = 2
|
||||
|
|
@ -419,6 +439,7 @@ unique_name_in_owner = true
|
|||
custom_minimum_size = Vector2(330, 48)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
focus_neighbor_top = NodePath("../SwapScrollToggle")
|
||||
focus_neighbor_bottom = NodePath("../../BindingButtons/ControllerMapping")
|
||||
toggle_mode = true
|
||||
text = "off"
|
||||
|
|
@ -567,7 +588,7 @@ size_flags_horizontal = 3
|
|||
focus_mode = 2
|
||||
focus_neighbor_right = NodePath("../ChangeDataFolder")
|
||||
focus_neighbor_top = NodePath("../../../../../../../../TabBar/DataTab")
|
||||
focus_neighbor_bottom = NodePath("../../ProgressionRow/ExportProgression")
|
||||
focus_neighbor_bottom = NodePath("../../CopyPlayerFingerprint")
|
||||
text = "open data folder"
|
||||
|
||||
[node name="ChangeDataFolder" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/FolderRow"]
|
||||
|
|
@ -578,41 +599,15 @@ size_flags_horizontal = 3
|
|||
focus_mode = 2
|
||||
focus_neighbor_left = NodePath("../OpenDataFolder")
|
||||
focus_neighbor_top = NodePath("../../../../../../../../TabBar/DataTab")
|
||||
focus_neighbor_bottom = NodePath("../../ProgressionRow/ImportProgression")
|
||||
focus_neighbor_bottom = NodePath("../../CopyPlayerFingerprint")
|
||||
text = "change data folder"
|
||||
|
||||
[node name="ProgressionRow" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="ExportProgression" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/ProgressionRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
focus_mode = 2
|
||||
focus_neighbor_right = NodePath("../ImportProgression")
|
||||
focus_neighbor_top = NodePath("../../FolderRow/OpenDataFolder")
|
||||
focus_neighbor_bottom = NodePath("../../CopyPlayerFingerprint")
|
||||
text = "export progression"
|
||||
|
||||
[node name="ImportProgression" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/ProgressionRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
focus_mode = 2
|
||||
focus_neighbor_left = NodePath("../ExportProgression")
|
||||
focus_neighbor_top = NodePath("../../FolderRow/ChangeDataFolder")
|
||||
focus_neighbor_bottom = NodePath("../../CopyPlayerFingerprint")
|
||||
text = "import progression"
|
||||
|
||||
[node name="CopyPlayerFingerprint" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 44)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
focus_neighbor_top = NodePath("../ProgressionRow/ExportProgression")
|
||||
focus_neighbor_top = NodePath("../FolderRow/OpenDataFolder")
|
||||
focus_neighbor_bottom = NodePath("../PlayerIdentityRow/ExportPlayerIdentity")
|
||||
text = "copy player fingerprint"
|
||||
|
||||
|
|
|
|||
|
|
@ -112,9 +112,9 @@ expand_icon = true
|
|||
[node name="ExportButton" type="Button" parent="TopPanel/Top"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "export aimed artwork as PNG"
|
||||
tooltip_text = "aim at artwork, then click to export PNG"
|
||||
focus_mode = 0
|
||||
accessibility_name = "export aimed artwork as PNG"
|
||||
accessibility_name = "export last aimed artwork as PNG"
|
||||
text = "png"
|
||||
|
||||
[node name="StampButton" type="Button" parent="TopPanel/Top"]
|
||||
|
|
|
|||
|
|
@ -352,6 +352,9 @@ func _build_job_rows(jobs: Array[Dictionary], empty_text: String) -> void:
|
|||
progress_bar.max_value = float(target)
|
||||
progress_bar.value = float(progress)
|
||||
progress_bar.show_percentage = false
|
||||
var exact_progress: String = "%d / %d" % [progress, target]
|
||||
progress_bar.tooltip_text = exact_progress
|
||||
progress_bar.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
progress_bar.add_theme_stylebox_override(
|
||||
"background", UtilityPageStyle.rounded_style(
|
||||
UtilityPageStyle.OCEAN_PANEL_DEEP, 9
|
||||
|
|
@ -372,7 +375,8 @@ func _build_job_rows(jobs: Array[Dictionary], empty_text: String) -> void:
|
|||
_compact_integer(progress),
|
||||
_compact_integer(target),
|
||||
]
|
||||
count.tooltip_text = "%d / %d" % [progress, target]
|
||||
count.tooltip_text = exact_progress
|
||||
count.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
count.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
count.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
count.add_theme_font_size_override("font_size", 14)
|
||||
|
|
@ -469,6 +473,7 @@ func _claim(claim_id: String) -> void:
|
|||
func _make_reward_display(fish_coin: int, experience: int) -> HBoxContainer:
|
||||
var reward := HBoxContainer.new()
|
||||
reward.tooltip_text = "%d fish coins · %d xp" % [fish_coin, experience]
|
||||
reward.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
reward.add_theme_constant_override("separation", 7)
|
||||
var currency: CurrencyAmount = (
|
||||
CurrencyPresentationType.instantiate_amount(fish_coin, 18.0)
|
||||
|
|
@ -498,6 +503,7 @@ func _make_job_reward_display(
|
|||
var reward := VBoxContainer.new()
|
||||
reward.custom_minimum_size.x = 104.0
|
||||
reward.tooltip_text = "%d fish coins · %d xp" % [fish_coin, experience]
|
||||
reward.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
reward.add_theme_constant_override("separation", 1)
|
||||
var currency: CurrencyAmount = (
|
||||
CurrencyPresentationType.instantiate_amount(fish_coin, 16.0)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=24 format=3]
|
||||
[gd_scene load_steps=25 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/title_screen.gd" id="1_script"]
|
||||
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
[ext_resource type="Texture2D" path="res://ui/icons/main_menu/credits.png" id="18_credits"]
|
||||
[ext_resource type="Texture2D" path="res://ui/icons/main_menu/delete_save.png" id="19_delete_save"]
|
||||
[ext_resource type="PackedScene" path="res://ui/new_game_setup_page.tscn" id="20_new_game_setup"]
|
||||
[ext_resource type="PackedScene" path="res://ui/save_slots_page.tscn" id="21_save_slots"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water"]
|
||||
shader = ExtResource("4_water_shader")
|
||||
|
|
@ -260,21 +261,21 @@ offset_bottom = 318.0
|
|||
script = ExtResource("8_bubble_cluster")
|
||||
profile = ExtResource("9_bubble_profile")
|
||||
|
||||
[node name="ContinueButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"]
|
||||
[node name="PlayButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"]
|
||||
unique_name_in_owner = true
|
||||
offset_left = 123.0
|
||||
offset_top = 86.0
|
||||
offset_right = 307.0
|
||||
offset_bottom = 264.0
|
||||
offset_left = 106.0
|
||||
offset_top = 71.0
|
||||
offset_right = 290.0
|
||||
offset_bottom = 249.0
|
||||
texture_filter = 1
|
||||
icon = ExtResource("16_continue")
|
||||
tooltip_text = "continue"
|
||||
accessibility_name = "continue"
|
||||
tooltip_text = "play"
|
||||
accessibility_name = "play"
|
||||
script = ExtResource("7_bubble_button")
|
||||
profile = ExtResource("9_bubble_profile")
|
||||
neutral_size = Vector2(184, 178)
|
||||
desktop_anchor = Vector2(215, 175)
|
||||
compact_anchor = Vector2(215, 175)
|
||||
desktop_anchor = Vector2(198, 160)
|
||||
compact_anchor = Vector2(198, 164)
|
||||
minimum_font_size = 19
|
||||
maximum_font_size = 30
|
||||
horizontal_amplitude = 1.8
|
||||
|
|
@ -285,6 +286,8 @@ deformation_period = 6.7
|
|||
|
||||
[node name="NewGameButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
focus_mode = 0
|
||||
offset_left = 42.0
|
||||
offset_top = 4.0
|
||||
offset_right = 170.0
|
||||
|
|
@ -310,18 +313,18 @@ deformation_period = 5.9
|
|||
|
||||
[node name="SettingsButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"]
|
||||
unique_name_in_owner = true
|
||||
offset_left = 264.0
|
||||
offset_top = 17.0
|
||||
offset_right = 384.0
|
||||
offset_bottom = 133.0
|
||||
offset_left = 254.0
|
||||
offset_top = 12.0
|
||||
offset_right = 374.0
|
||||
offset_bottom = 128.0
|
||||
texture_filter = 1
|
||||
icon = ExtResource("13_settings_dark")
|
||||
tooltip_text = "settings"
|
||||
accessibility_name = "settings"
|
||||
script = ExtResource("7_bubble_button")
|
||||
profile = ExtResource("9_bubble_profile")
|
||||
desktop_anchor = Vector2(324, 75)
|
||||
compact_anchor = Vector2(324, 68)
|
||||
desktop_anchor = Vector2(314, 70)
|
||||
compact_anchor = Vector2(333, 75)
|
||||
compact_minimum_size = Vector2(94, 90)
|
||||
minimum_font_size = 15
|
||||
maximum_font_size = 22
|
||||
|
|
@ -332,10 +335,10 @@ deformation_period = 6.3
|
|||
|
||||
[node name="CreditsButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"]
|
||||
unique_name_in_owner = true
|
||||
offset_left = 330.0
|
||||
offset_top = 125.0
|
||||
offset_right = 414.0
|
||||
offset_bottom = 207.0
|
||||
offset_left = 54.0
|
||||
offset_top = 209.0
|
||||
offset_right = 138.0
|
||||
offset_bottom = 291.0
|
||||
texture_filter = 1
|
||||
icon = ExtResource("18_credits")
|
||||
tooltip_text = "credits"
|
||||
|
|
@ -343,8 +346,8 @@ accessibility_name = "credits"
|
|||
script = ExtResource("7_bubble_button")
|
||||
profile = ExtResource("9_bubble_profile")
|
||||
neutral_size = Vector2(84, 82)
|
||||
desktop_anchor = Vector2(372, 166)
|
||||
compact_anchor = Vector2(372, 166)
|
||||
desktop_anchor = Vector2(96, 250)
|
||||
compact_anchor = Vector2(82, 258)
|
||||
compact_minimum_size = Vector2(62, 62)
|
||||
minimum_font_size = 12
|
||||
maximum_font_size = 17
|
||||
|
|
@ -357,10 +360,10 @@ deformation_period = 5.6
|
|||
|
||||
[node name="JoinGameButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"]
|
||||
unique_name_in_owner = true
|
||||
offset_left = 126.0
|
||||
offset_top = 224.0
|
||||
offset_right = 248.0
|
||||
offset_bottom = 342.0
|
||||
offset_left = 21.0
|
||||
offset_top = 11.0
|
||||
offset_right = 143.0
|
||||
offset_bottom = 129.0
|
||||
texture_filter = 1
|
||||
icon = ExtResource("12_online_dark")
|
||||
tooltip_text = "join game"
|
||||
|
|
@ -368,8 +371,8 @@ accessibility_name = "join game"
|
|||
script = ExtResource("7_bubble_button")
|
||||
profile = ExtResource("9_bubble_profile")
|
||||
neutral_size = Vector2(122, 118)
|
||||
desktop_anchor = Vector2(187, 268)
|
||||
compact_anchor = Vector2(187, 268)
|
||||
desktop_anchor = Vector2(82, 70)
|
||||
compact_anchor = Vector2(63, 75)
|
||||
compact_minimum_size = Vector2(82, 80)
|
||||
minimum_font_size = 15
|
||||
maximum_font_size = 22
|
||||
|
|
@ -382,6 +385,8 @@ deformation_period = 5.7
|
|||
|
||||
[node name="DeleteSaveButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
focus_mode = 0
|
||||
offset_left = -11.0
|
||||
offset_top = 122.0
|
||||
offset_right = 123.0
|
||||
|
|
@ -406,10 +411,10 @@ deformation_period = 5.4
|
|||
|
||||
[node name="QuitButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"]
|
||||
unique_name_in_owner = true
|
||||
offset_left = 275.0
|
||||
offset_top = 217.0
|
||||
offset_right = 369.0
|
||||
offset_bottom = 309.0
|
||||
offset_left = 255.0
|
||||
offset_top = 204.0
|
||||
offset_right = 349.0
|
||||
offset_bottom = 296.0
|
||||
texture_filter = 1
|
||||
icon = ExtResource("14_x_dark")
|
||||
tooltip_text = "quit"
|
||||
|
|
@ -417,8 +422,8 @@ accessibility_name = "quit"
|
|||
script = ExtResource("7_bubble_button")
|
||||
profile = ExtResource("9_bubble_profile")
|
||||
neutral_size = Vector2(94, 92)
|
||||
desktop_anchor = Vector2(322, 263)
|
||||
compact_anchor = Vector2(322, 263)
|
||||
desktop_anchor = Vector2(302, 250)
|
||||
compact_anchor = Vector2(317, 254)
|
||||
compact_minimum_size = Vector2(68, 68)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 18
|
||||
|
|
@ -497,3 +502,13 @@ anchor_right = 1.0
|
|||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="SaveSlotsPage" parent="ResponsiveTitleStage/TitlePresentationScaleRoot" instance=ExtResource("21_save_slots")]
|
||||
unique_name_in_owner = true
|
||||
z_index = 220
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ func _ready() -> void:
|
|||
_game_ui.set_controller_text_entry_request(
|
||||
Callable(_on_screen_keyboard, "request_for_control"),
|
||||
Callable(_on_screen_keyboard, "is_open"),
|
||||
Callable(_on_screen_keyboard, "close_for_control"),
|
||||
)
|
||||
var controller_focus_presentation := ControllerFocusPresentationType.new()
|
||||
_ui_root.add_child(controller_focus_presentation)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue