Expand gameplay systems and interface controls

This commit is contained in:
Alexander Sellite 2026-08-20 17:53:22 -04:00
parent 09b7a78533
commit 2710544070
46 changed files with 2072 additions and 153 deletions

View file

@ -897,6 +897,8 @@ func _send() -> void:
if body.strip_edges().is_empty():
close_chat()
return
if _handle_editor_world_command(body):
return
_send_pending = true
_pending_send_body = NetworkChatProtocol.sanitize_body(body)
_entry.editable = false
@ -912,6 +914,82 @@ func _send() -> void:
_set_status("Sending…")
func _handle_editor_world_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.
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)
var command: String = String(parts[0]).trim_prefix("/").to_lower()
var result: String = ""
if _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
_entry.clear()
_flush_draft()
_set_status(result)
close_chat(true)
return true
func _apply_editor_time_command(parts: PackedStringArray) -> String:
if parts.size() != 2 or _world_time == null:
return "Usage: /time [dawn, day, dusk, night]"
var phase_name: String = String(parts[1]).to_lower()
var target_hour: float = -1.0
match phase_name:
"dawn":
target_hour = WorldTimeServiceType.DAWN_START_HOUR
"day":
target_hour = WorldTimeServiceType.DAWN_END_HOUR
"dusk":
target_hour = WorldTimeServiceType.DUSK_START_HOUR
"night":
target_hour = WorldTimeServiceType.DUSK_END_HOUR
_:
return "Usage: /time [dawn, day, dusk, night]"
if not _world_time.set_authoritative_time(target_hour):
return "Editor world time could not be changed."
return "Editor time: %s (%s)." % [
phase_name,
_world_time.get_clock_text(),
]
func _apply_editor_weather_command(parts: PackedStringArray) -> String:
if parts.size() != 2 or _world_weather == null:
return "Usage: /weather [sunny, cloudy, rainy, foggy]"
var weather_name: String = String(parts[1]).to_lower()
var target_weather: WorldWeatherServiceType.Weather
match weather_name:
"clear", "sunny":
weather_name = "sunny"
target_weather = WorldWeatherServiceType.Weather.SUNNY
"cloudy":
target_weather = WorldWeatherServiceType.Weather.CLOUDY
"rainy":
target_weather = WorldWeatherServiceType.Weather.RAINY
"foggy":
target_weather = WorldWeatherServiceType.Weather.FOGGY
_:
return "Usage: /weather [sunny, cloudy, rainy, foggy]"
if not _world_weather.set_authoritative_weather(target_weather):
return "Editor world weather could not be changed."
return "Editor weather: %s." % weather_name
func _on_local_message_confirmed(message: Dictionary) -> void:
if (
not _send_pending

View file

@ -168,6 +168,12 @@ func _on_pressed() -> void:
func _on_gui_input(event: InputEvent) -> void:
var mouse_event := event as InputEventMouseButton
if mouse_event != null and mouse_event.pressed:
# Pointer clicks give Buttons keyboard focus after their GUI callback.
# Release that pointer-created focus on the next frame so a tooltip does
# not remain pinned after the pointer leaves. Controller navigation still
# keeps focus normally because it does not arrive as a mouse event.
call_deferred("_release_pointer_focus")
if (
mouse_event != null
and mouse_event.button_index == MOUSE_BUTTON_RIGHT
@ -179,6 +185,11 @@ func _on_gui_input(event: InputEvent) -> void:
accept_event()
func _release_pointer_focus() -> void:
if has_focus():
release_focus()
func _set_context_hovered(active: bool) -> void:
_context_hovered = active
_update_context_presence()

View file

@ -214,6 +214,7 @@ var _controller_text_entry_is_open: Callable
func _ready() -> void:
_prioritize_surface_drawing_pointer_input()
_bite_prompt_button.pressed.connect(_on_bite_prompt_pressed)
_apply_active_bait_indicator_style()
_refresh_active_bait_indicator()
@ -487,6 +488,27 @@ func setup(
)
func _prioritize_surface_drawing_pointer_input() -> void:
# Control input follows sibling order rather than CanvasItem.z_index. Chat is
# a full-screen Control with interactive mobile children, so the toolbar must
# follow it in the tree to own their overlap while the art kit is active.
# Opening Chat deactivates surface drawing and hides the toolbar, returning
# the same area to Chat without any special-case pointer forwarding.
if (
_surface_drawing_toolbar == null
or _chat_ui == null
or _surface_drawing_toolbar.get_parent() != _chat_ui.get_parent()
):
return
var ui_root: Node = _surface_drawing_toolbar.get_parent()
var toolbar_index: int = _surface_drawing_toolbar.get_index()
var chat_index: int = _chat_ui.get_index()
if toolbar_index > chat_index:
return
# Removing the earlier toolbar shifts Chat one position toward the start.
ui_root.move_child(_surface_drawing_toolbar, chat_index)
func _input(event: InputEvent) -> void:
# The on-screen keyboard owns controller input while it is open. Its
# overlay is processed before the UI beneath it and consumes the event.
@ -1264,6 +1286,7 @@ 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,
@ -1275,6 +1298,7 @@ func setup_data_and_identity(
]:
panel.setup_data_and_identity(
data_root,
progression_saves,
identity_backups,
player_identity,
host_identity,
@ -1603,9 +1627,14 @@ func _can_toggle_gameplay_hud() -> bool:
func _refresh_gameplay_hud_visibility() -> void:
var fishing_override_active: bool = (
_gameplay_hud_hidden
and _fishing_spot != null
and _fishing_spot.is_fishing_sequence_active()
)
var show_world_hud: bool = (
_gameplay_ui_enabled
and not _gameplay_hud_hidden
and (not _gameplay_hud_hidden or fishing_override_active)
and not _system_menu_open
and not _player_menu_open
and not _shop_open
@ -1660,6 +1689,7 @@ func set_storage_prompt_visible(
_storage_prompt.visible = (
requested_visible
and _gameplay_ui_enabled
and not _gameplay_hud_hidden
and not _system_menu_open
and not _player_menu_open
and not _shop_open
@ -1697,6 +1727,7 @@ func set_shop_prompt_visible(
_shop_prompt.visible = (
requested_visible
and _gameplay_ui_enabled
and not _gameplay_hud_hidden
and not _system_menu_open
and not _player_menu_open
and not _shop_open
@ -1923,6 +1954,7 @@ func _refresh_fishing_panel_visibility() -> void:
_gameplay_ui_enabled
and has_content
)
_refresh_gameplay_hud_visibility()
func _on_bite_prompt_changed(prompt_visible: bool) -> void:

View file

@ -430,7 +430,8 @@ func _show_native_keyboard_for(control: Control) -> void:
func _hide_native_keyboard() -> void:
DisplayServer.virtual_keyboard_hide()
if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD):
DisplayServer.virtual_keyboard_hide()
static func _line_edit_keyboard_type(

View file

@ -1693,6 +1693,7 @@ func close_menu(
_release_controller_ownership(false, true)
get_viewport().gui_cancel_drag()
_close_sale_confirmation()
_hide_inventory_context_tooltip()
if reason in [
CloseReason.BITE_STARTED,
CloseReason.WATER_RECOVERY,
@ -2497,6 +2498,8 @@ func _populate_tackle_column(
owned.quantity,
item.max_stack,
&"QuantityBadge",
UtilityPageStyle.SUPPLY_BADGE_EDGE_MARGIN,
0.0,
)
button.pressed.connect(_select_tackle_item.bind(owned.item_id))
button.gui_input.connect(
@ -2540,6 +2543,8 @@ func _on_tackle_button_gui_input(
button: Button,
) -> void:
var mouse_event := event as InputEventMouseButton
if mouse_event != null and mouse_event.pressed:
call_deferred("_release_pointer_focus", button)
if (
mouse_event == null
or mouse_event.button_index != MOUSE_BUTTON_RIGHT
@ -2555,6 +2560,11 @@ func _on_tackle_button_gui_input(
button.accept_event()
func _release_pointer_focus(control: Control) -> void:
if control != null and is_instance_valid(control) and control.has_focus():
control.release_focus()
func _configure_tackle_item_focus() -> void:
if not is_node_ready():
return

View file

@ -125,6 +125,7 @@ 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
@ -134,6 +135,8 @@ 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
@ -143,6 +146,7 @@ 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:
@ -244,6 +248,8 @@ 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")
)
@ -337,6 +343,7 @@ 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,
@ -344,6 +351,7 @@ 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
@ -611,6 +619,11 @@ 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 ""
)
@ -661,6 +674,118 @@ 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 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)),
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."

View file

@ -567,7 +567,7 @@ size_flags_horizontal = 3
focus_mode = 2
focus_neighbor_right = NodePath("../ChangeDataFolder")
focus_neighbor_top = NodePath("../../../../../../../../TabBar/DataTab")
focus_neighbor_bottom = NodePath("../../CopyPlayerFingerprint")
focus_neighbor_bottom = NodePath("../../ProgressionRow/ExportProgression")
text = "open data folder"
[node name="ChangeDataFolder" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/FolderRow"]
@ -578,15 +578,41 @@ size_flags_horizontal = 3
focus_mode = 2
focus_neighbor_left = NodePath("../OpenDataFolder")
focus_neighbor_top = NodePath("../../../../../../../../TabBar/DataTab")
focus_neighbor_bottom = NodePath("../../CopyPlayerFingerprint")
focus_neighbor_bottom = NodePath("../../ProgressionRow/ImportProgression")
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("../FolderRow/OpenDataFolder")
focus_neighbor_top = NodePath("../ProgressionRow/ExportProgression")
focus_neighbor_bottom = NodePath("../PlayerIdentityRow/ExportPlayerIdentity")
text = "copy player fingerprint"

View file

@ -1295,7 +1295,7 @@ func _on_settings_applied() -> void:
func _on_settings_closed() -> void:
pass
_refresh_save_inspection()
func _on_settings_closing() -> void:

View file

@ -53,6 +53,7 @@ static func add_supply_quantity_badge(
total: int,
badge_name: StringName = &"SupplyQuantityBadge",
top_margin: float = SUPPLY_BADGE_EDGE_MARGIN,
x_offset: float = SUPPLY_BADGE_X_OFFSET,
) -> Panel:
var badge := Panel.new()
badge.name = badge_name
@ -68,7 +69,7 @@ static func add_supply_quantity_badge(
quantity_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
badge.add_child(quantity_label)
configure_supply_quantity_badge(
badge, quantity_label, current, total, top_margin
badge, quantity_label, current, total, top_margin, x_offset
)
return badge
@ -79,6 +80,7 @@ static func configure_supply_quantity_badge(
current: int,
total: int,
top_margin: float = SUPPLY_BADGE_EDGE_MARGIN,
x_offset: float = SUPPLY_BADGE_X_OFFSET,
) -> void:
var supply_text := supply_quantity_text(current, total)
var text_width: float = TuffyFont.get_string_size(
@ -92,10 +94,10 @@ static func configure_supply_quantity_badge(
ceilf(text_width + SUPPLY_BADGE_HORIZONTAL_PADDING),
)
badge.offset_left = (
-badge_width - SUPPLY_BADGE_EDGE_MARGIN + SUPPLY_BADGE_X_OFFSET
-badge_width - SUPPLY_BADGE_EDGE_MARGIN + x_offset
)
badge.offset_top = top_margin
badge.offset_right = -SUPPLY_BADGE_EDGE_MARGIN + SUPPLY_BADGE_X_OFFSET
badge.offset_right = -SUPPLY_BADGE_EDGE_MARGIN + x_offset
badge.offset_bottom = top_margin + SUPPLY_BADGE_HEIGHT
badge.pivot_offset = Vector2(
badge_width * 0.5,