feat: expand progression and multiplayer systems

Add named save slots, progression import/export, and a unified play flow. Add live friend requests, presence, invitations, and relationship controls without durable discovery-server social storage. Advance the network protocol with isolated channels, movement reconciliation, late-join recovery, fishing replication, and animation synchronization. Preserve per-species catch totals, refine generated-world startup and water recovery, and complete the related input and interface improvements.
This commit is contained in:
Alexander Sellite 2026-08-23 20:48:38 -04:00
parent 1db1a5b754
commit 3b84bfe3a0
97 changed files with 7869 additions and 982 deletions

View file

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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View 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

View file

@ -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(),

View file

@ -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":

View file

@ -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")]

View file

@ -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

View file

@ -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
View 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()

View file

@ -0,0 +1 @@
uid://cytwdvxf22106

350
ui/save_slots_page.tscn Normal file
View 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"

View file

@ -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()

View file

@ -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"

View file

@ -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"]

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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)