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.
731 lines
24 KiB
GDScript
731 lines
24 KiB
GDScript
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()
|