Integrate per-save identity and interaction UI

This commit is contained in:
Alexander Sellite 2026-09-01 17:30:50 -04:00
parent ac82a28f3b
commit 4fda6e97f6
58 changed files with 3471 additions and 461 deletions

View file

@ -25,9 +25,21 @@ const SPEECH_BUBBLE_WIDTH: float = 320.0
const SPEECH_POINTER_HALF_WIDTH: float = 12.0
const SPEECH_POINTER_HEIGHT: float = 14.0
const SPEECH_POINTER_OVERLAP: float = 3.0
const NAMEPLATE_WIDTH: float = 300.0
const NAMEPLATE_NAME_HEIGHT: float = 24.0
const NAMEPLATE_TITLE_TOP: float = 21.0
const NAMEPLATE_TITLE_HEIGHT: float = 18.0
const NAMEPLATE_HEIGHT: float = (
NAMEPLATE_TITLE_TOP + NAMEPLATE_TITLE_HEIGHT
)
const NAMEPLATE_NAME_FONT_SIZE: int = 20
const NAMEPLATE_TITLE_FONT_SIZE: int = 14
const NAMEPLATE_HEAD_GAP: float = 3.0
const NAMEPLATE_SPEECH_GAP: float = 5.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 ROLEPLAY_FONT_SLANT: float = -0.18
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
@ -107,6 +119,7 @@ var _player: Player
var _fishing_spot: FishingSpot
var _settings: PlayerSettingsManager
var _history: RichTextLabel
var _roleplay_font: FontVariation
var _entry: LineEdit
var _status: Label
var _panel: PanelContainer
@ -114,6 +127,7 @@ var _collapse_button: Button
var _height_button: Button
var _unread_indicator: Label
var _hint: Label
var _nameplate_layer: Control
var _speech_layer: Control
var _animalese_voice: AnimaleseVoiceType
var _clock_panel: PanelContainer
@ -125,6 +139,7 @@ var _calendar_balance: CurrencyAmount
var _weather_icon: WeatherIconType
var _status_effect_column: VBoxContainer
var _status_effect_icons: Dictionary[StringName, TextureRect] = {}
var _nameplates: Dictionary[int, Dictionary] = {}
var _speech: Dictionary[int, Dictionary] = {}
var _draft_save_timer: Timer
var _opacity_tween: Tween
@ -132,6 +147,7 @@ var _height_tween: Tween
var _opened: bool = false
var _available: bool = false
var _hud_hidden: bool = false
var _nameplates_hud_hidden: bool = false
var _world_speech_visible: bool = true
var _presentation_state := PresentationState.COMPACT
var _visible_state_before_collapse := PresentationState.COMPACT
@ -247,7 +263,12 @@ func setup(
_service.local_message_confirmed.connect(_on_local_message_confirmed)
_service.history_replaced.connect(_on_history)
_service.send_rejected.connect(_on_rejected)
_session.peer_removed.connect(_on_peer_removed)
_session.peer_authenticated.connect(_on_peer_authenticated)
_session.peer_display_name_changed.connect(_on_peer_display_name_changed)
_session.peer_title_changed.connect(_on_peer_title_changed)
_session.peer_removed.connect(_on_session_peer_removed)
_spawn.avatar_spawned.connect(_on_avatar_spawned)
_spawn.avatar_removed.connect(_on_avatar_removed)
_entry.text = _settings.current_settings.chat_draft
_entry.caret_column = _entry.text.length()
_set_presentation_state(
@ -258,6 +279,7 @@ func setup(
false,
)
_refresh_history()
_reconcile_nameplates()
func _rebuild_status_effect_icons() -> void:
@ -370,6 +392,7 @@ func open_command_chat() -> void:
func set_available(value: bool) -> void:
_available = value
_refresh_nameplate_layer_visibility()
if not value:
_clear_suspended_chat_state()
_send_pending = false
@ -601,6 +624,7 @@ func _process(_delta: float) -> void:
_refresh_visibility()
_update_status_effect_icons()
_update_panel_opacity()
_update_nameplates()
_update_speech()
@ -612,6 +636,11 @@ func _exit_tree() -> void:
func _build_ui() -> void:
_roleplay_font = FontVariation.new()
_roleplay_font.base_font = UtilityPageStyle.TuffyFont
var roleplay_transform := Transform2D.IDENTITY
roleplay_transform.y.x = ROLEPLAY_FONT_SLANT
_roleplay_font.variation_transform = roleplay_transform
_clock_panel = PanelContainer.new()
_clock_panel.name = "WorldClockPanel"
_clock_panel.size = CLOCK_SIZE
@ -720,7 +749,9 @@ func _build_ui() -> void:
_history.scroll_following = true
_history.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
_history.mouse_filter = Control.MOUSE_FILTER_STOP
_history.bbcode_enabled = false
_history.add_theme_font_override("normal_font", UtilityPageStyle.TuffyFont)
_history.add_theme_font_override("italics_font", _roleplay_font)
_history.add_theme_font_size_override("normal_font_size", HISTORY_FONT_SIZE)
_history.add_theme_color_override("default_color", UtilityPageStyle.LIGHT_TEXT)
_history.add_theme_stylebox_override("normal", _borderless_style(Color.TRANSPARENT))
@ -846,7 +877,16 @@ func _build_ui() -> void:
_hint.add_theme_constant_override("shadow_offset_x", 1)
_hint.add_theme_constant_override("shadow_offset_y", 2)
add_child(_hint)
_nameplate_layer = Control.new()
_nameplate_layer.name = "PlayerNameplateLayer"
_nameplate_layer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_nameplate_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
_nameplate_layer.visible = (
_available and _world_speech_visible and not _nameplates_hud_hidden
)
add_child(_nameplate_layer)
_speech_layer = Control.new()
_speech_layer.name = "PlayerSpeechLayer"
_speech_layer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_speech_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_speech_layer)
@ -1193,10 +1233,15 @@ func _on_message(message: Dictionary) -> void:
_update_panel_opacity(true)
if int(message["kind"]) != NetworkChatProtocol.Kind.PLAYER:
return
var body: String = str(message["body"])
# Roleplay actions belong only in chat history. They deliberately do not
# create world speech, animalese, or a talking animation.
if NetworkChatProtocol.is_roleplay_body(body):
return
var peer_id: int = message["sender_peer_id"]
_show_speech_bubble(
peer_id,
str(message["body"]),
body,
str(message.get("sender_fingerprint", "")),
VoiceProfilesType.sanitized_id(str(message.get(
"voice_id", VoiceProfilesType.DEFAULT_ID
@ -1221,6 +1266,8 @@ func _show_speech_bubble(
requested_sample_set_id: String = "",
) -> void:
_on_peer_removed(peer_id)
if not _speaker_shares_local_space(peer_id):
return
var bubble := PanelContainer.new()
bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
bubble.custom_minimum_size = Vector2(SPEECH_BUBBLE_WIDTH, 0.0)
@ -1342,16 +1389,27 @@ func _on_rejected(message: String) -> void:
func _refresh_history() -> void:
if _service == null:
return
var lines: Array[String] = []
var messages := _service.get_history()
for message: Dictionary in messages:
_history.clear()
for message_index: int in messages.size():
var message: Dictionary = messages[message_index]
if message_index > 0:
_history.add_text("\n")
if int(message["kind"]) == NetworkChatProtocol.Kind.SYSTEM:
lines.append("%s" % str(message["body"]))
else:
lines.append("%s: %s" % [
str(message["sender_display_name"]), str(message["body"]),
_history.add_text("%s" % str(message["body"]))
continue
var body: String = str(message["body"])
if NetworkChatProtocol.is_roleplay_body(body):
_history.push_italics()
_history.add_text("%s %s" % [
str(message["sender_display_name"]),
NetworkChatProtocol.roleplay_action(body),
])
_history.text = "\n".join(lines)
_history.pop()
continue
_history.add_text("%s: %s" % [
str(message["sender_display_name"]), body,
])
_scroll_history_to_bottom.call_deferred()
@ -1465,6 +1523,8 @@ func is_mobile_mode() -> bool:
func set_hud_hidden(is_hidden: bool) -> void:
_hud_hidden = is_hidden
_nameplates_hud_hidden = is_hidden
_refresh_nameplate_layer_visibility()
_refresh_visibility()
@ -1472,8 +1532,14 @@ func is_hud_hidden() -> bool:
return _hud_hidden
func set_world_nameplates_hud_hidden(is_hidden: bool) -> void:
_nameplates_hud_hidden = is_hidden
_refresh_nameplate_layer_visibility()
func set_world_speech_visible(should_be_visible: bool) -> void:
_world_speech_visible = should_be_visible
_refresh_nameplate_layer_visibility()
if _speech_layer != null:
_speech_layer.visible = should_be_visible
@ -1482,6 +1548,14 @@ func is_world_speech_visible() -> bool:
return _world_speech_visible
func _refresh_nameplate_layer_visibility() -> void:
if _nameplate_layer == null:
return
_nameplate_layer.visible = (
_available and _world_speech_visible and not _nameplates_hud_hidden
)
func _refresh_handle_labels(state: PresentationState) -> void:
var collapsed := state == PresentationState.COLLAPSED
var height_state := _visible_state_before_collapse if collapsed else state
@ -1538,11 +1612,217 @@ func _update_panel_opacity(immediate_recheck: bool = false) -> void:
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
func _update_nameplates() -> void:
_reconcile_nameplates()
if _spawn == null or _player == null:
_hide_all_nameplates()
return
var camera := _player.get_active_gameplay_camera()
if camera == null:
_hide_all_nameplates()
return
var viewport_size := get_viewport_rect().size
for peer_id: int in _nameplates.keys():
var state: Dictionary = _nameplates[peer_id]
var plate := state.get("control") as Control
var avatar := _spawn.get_avatar(peer_id)
if plate == null:
continue
if (
avatar == null
or not _available
or _nameplates_hud_hidden
or not _world_speech_visible
or not _speaker_shares_local_space(peer_id)
or not _avatar_presentation_visible(peer_id, avatar)
):
plate.hide()
continue
var world_position := avatar.get_nameplate_anchor_position()
if (
camera.is_position_behind(world_position)
or _is_speech_world_occluded(camera, avatar, world_position)
):
plate.hide()
continue
var screen_position := (
camera.unproject_position(world_position) / _output_scale
)
if (
screen_position.x < -NAMEPLATE_WIDTH * 0.5
or screen_position.x > viewport_size.x + NAMEPLATE_WIDTH * 0.5
or screen_position.y < -NAMEPLATE_HEIGHT
or screen_position.y > viewport_size.y + NAMEPLATE_HEIGHT
):
plate.hide()
continue
plate.position = Vector2(
screen_position.x - NAMEPLATE_WIDTH * 0.5,
screen_position.y - NAMEPLATE_HEIGHT - NAMEPLATE_HEAD_GAP,
)
plate.show()
func _reconcile_nameplates() -> void:
if _spawn == null or _session == null or _nameplate_layer == null:
return
var live_peer_ids: Dictionary[int, bool] = {}
for peer_id: int in _spawn.get_peer_ids():
if (
_spawn.get_avatar(peer_id) == null
or _session.get_peer_record(peer_id) == null
):
continue
live_peer_ids[peer_id] = true
_ensure_nameplate(peer_id)
for peer_id: int in _nameplates.keys():
if not live_peer_ids.has(peer_id):
_remove_nameplate(peer_id)
func _ensure_nameplate(peer_id: int) -> void:
if _nameplates.has(peer_id):
_refresh_nameplate_text(peer_id)
return
var plate := Control.new()
plate.name = "PlayerNameplate_%d" % peer_id
plate.size = Vector2(NAMEPLATE_WIDTH, NAMEPLATE_HEIGHT)
plate.mouse_filter = Control.MOUSE_FILTER_IGNORE
plate.hide()
var name_label := _create_nameplate_label(
"PlayerName",
NAMEPLATE_NAME_FONT_SIZE,
0.0,
NAMEPLATE_NAME_HEIGHT,
)
var title_label := _create_nameplate_label(
"PlayerTitle",
NAMEPLATE_TITLE_FONT_SIZE,
NAMEPLATE_TITLE_TOP,
NAMEPLATE_TITLE_HEIGHT,
)
plate.add_child(name_label)
plate.add_child(title_label)
_nameplate_layer.add_child(plate)
_nameplates[peer_id] = {
"control": plate,
"name": name_label,
"title": title_label,
}
_refresh_nameplate_text(peer_id)
func _create_nameplate_label(
node_name: String,
font_size: int,
vertical_position: float,
height: float,
) -> Label:
var label := Label.new()
label.name = node_name
label.position = Vector2(0.0, vertical_position)
label.size = Vector2(NAMEPLATE_WIDTH, height)
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.clip_text = true
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", Color.WHITE)
label.add_theme_color_override("font_outline_color", Color.TRANSPARENT)
label.add_theme_color_override("font_shadow_color", Color.TRANSPARENT)
label.add_theme_constant_override("outline_size", 0)
label.add_theme_constant_override("shadow_offset_x", 0)
label.add_theme_constant_override("shadow_offset_y", 0)
return label
func _refresh_nameplate_text(peer_id: int) -> void:
if _session == null or not _nameplates.has(peer_id):
return
var record := _session.get_peer_record(peer_id)
if record == null:
return
var state: Dictionary = _nameplates[peer_id]
var name_label := state.get("name") as Label
var title_label := state.get("title") as Label
if name_label != null:
name_label.text = (
record.display_name
if not record.display_name.is_empty()
else NetworkProfilePreferences.DEFAULT_DISPLAY_NAME
)
if title_label != null:
title_label.text = (
record.title
if not record.title.is_empty()
else NetworkProfilePreferences.DEFAULT_TITLE
)
func _remove_nameplate(peer_id: int) -> void:
var state: Dictionary = _nameplates.get(peer_id, {})
var plate := state.get("control") as Control
if plate != null:
plate.queue_free()
_nameplates.erase(peer_id)
func _hide_all_nameplates() -> void:
for state: Dictionary in _nameplates.values():
var plate := state.get("control") as Control
if plate != null:
plate.hide()
func _avatar_presentation_visible(peer_id: int, avatar: Player) -> bool:
if _session == null or peer_id == _session.get_local_peer_id():
return true
return avatar.is_remote_presentation_visible()
func _nameplate_speech_clearance(peer_id: int) -> float:
var state: Dictionary = _nameplates.get(peer_id, {})
var plate := state.get("control") as Control
if plate == null or not plate.is_visible_in_tree():
return 0.0
return NAMEPLATE_HEIGHT + NAMEPLATE_HEAD_GAP + NAMEPLATE_SPEECH_GAP
func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
_ensure_nameplate(peer_id)
func _on_peer_display_name_changed(peer_id: int, _display_name: String) -> void:
_ensure_nameplate(peer_id)
func _on_peer_title_changed(peer_id: int, _title: String) -> void:
_ensure_nameplate(peer_id)
func _on_avatar_spawned(peer_id: int, _avatar: Player) -> void:
_ensure_nameplate(peer_id)
func _on_avatar_removed(peer_id: int) -> void:
_remove_nameplate(peer_id)
func _on_session_peer_removed(peer_id: int) -> void:
_on_peer_removed(peer_id)
_remove_nameplate(peer_id)
func _update_speech() -> void:
var now := Time.get_ticks_msec() / 1000.0
for peer_id: int in _speech.keys():
var state: Dictionary = _speech[peer_id]
var bubble := state.get("bubble") as PanelContainer
if not _speaker_shares_local_space(peer_id):
_on_peer_removed(peer_id)
continue
if bubble == null or now >= float(state.get("expires", 0.0)):
if bubble != null:
bubble.queue_free()
@ -1553,10 +1833,14 @@ func _update_speech() -> void:
continue
var avatar := _spawn.get_avatar(peer_id)
var camera := _player.get_active_gameplay_camera()
if avatar == null or camera == null:
if (
avatar == null
or camera == null
or not _avatar_presentation_visible(peer_id, avatar)
):
bubble.hide()
continue
var world_position := avatar.get_chat_anchor_position()
var world_position := avatar.get_nameplate_anchor_position()
if camera.is_position_behind(world_position):
bubble.hide()
continue
@ -1566,6 +1850,7 @@ func _update_speech() -> void:
var screen_position := (
camera.unproject_position(world_position) / _output_scale
)
screen_position.y -= _nameplate_speech_clearance(peer_id)
var viewport_size := get_viewport_rect().size
if (
screen_position.x < -80.0
@ -1631,10 +1916,27 @@ func _is_speech_world_occluded(
if collider is Player:
excluded.append(collider.get_rid())
continue
if (
collider != null
and bool(collider.get_meta(
HomeWorldService.INTERIOR_SHELL_COLLISION_META, false
))
):
excluded.append(collider.get_rid())
continue
return true
return false
func _speaker_shares_local_space(peer_id: int) -> bool:
if _session == null:
return true
var local_peer_id: int = _session.get_local_peer_id()
if local_peer_id <= 0:
return true
return _session.peers_share_space(local_peer_id, peer_id)
func _on_draft_changed(_value: String) -> void:
if _settings == null:
return

View file

@ -88,8 +88,6 @@ signal pixelation_settings_visibility_changed(is_visible: bool)
signal crisp_reset_focus_requested
signal interactive_pointer_ui_changed(is_open: bool)
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
@ -119,6 +117,7 @@ const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0
@onready var _status_label: Label = %StatusLabel
@onready var _bite_prompt_button: Button = %BitePromptButton
@onready var _gameplay_transient_hud: Control = %GameplayTransientHUD
@onready var _gameplay_screen_effects: Control = %GameplayScreenEffects
@onready var _active_bait_indicator: Control = %ActiveBaitIndicator
@onready var _active_bait_button: Button = %ActiveBaitButton
@onready var _active_bait_quantity_badge: Panel = %ActiveBaitQuantityBadge
@ -148,7 +147,8 @@ const VoiceProfilesType = preload(
const SHOP_ANIMALESE_VOICE_ID: String = "natural"
const SHOP_ANIMALESE_BASE_PITCH: float = 1.08
const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0
const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
const SHOP_PROMPT_DIALOGUE_SIZE := Vector2(300.0, 72.0)
const SHOP_PROMPT_COMPACT_SIZE := Vector2(30.0, 30.0)
const FISHING_DANGER_START_GAP: float = 0.15
const FISHING_DANGER_FULL_GAP: float = 0.025
@ -175,9 +175,11 @@ const FISHING_DANGER_FULL_GAP: float = 0.025
@onready var _shop_prompt_key: Label = %ShopPromptKey
@onready var _shop_prompt_pointer: Polygon2D = %ShopPromptPointer
var _shop_animalese_voice: AnimaleseVoiceType
var _shop_speech_tween: Tween
var _shop_npc_player_in_range: bool = false
var _shop_npc_dialogue_active: bool = false
var _shop_npc_spoken_for_current_visit: bool = false
var _shop_npc_next_speech_msec: int = 0
var _shop_npc_source_id: int = 0
@onready var _chat_ui: ChatUIType = %ChatUI
@onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu
@onready var _quick_radial_menu: QuickRadialMenuType = %QuickRadialMenu
@ -559,11 +561,8 @@ func setup(
_on_storage_visibility_changed
)
_fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
_fishing_shop.menu_exit_started.connect(_on_shop_exit_started)
_decor_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
_decor_shop.menu_exit_started.connect(_on_shop_exit_started)
_rv_upgrade_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
_rv_upgrade_shop.menu_exit_started.connect(_on_shop_exit_started)
_fishing_shop.sell_fish_requested.connect(_on_shop_sell_fish_requested)
_fishing_shop.shop_cooler_return_requested.connect(
_on_shop_cooler_return_requested
@ -700,6 +699,7 @@ func _input(event: InputEvent) -> void:
can_open and not _quick_radial_menu.is_open(),
):
var emote_is_open: bool = _emote_radial_menu.is_open()
_refresh_home_decor_toolbar_obscuring()
if emote_is_open != emote_was_open and _player != null:
_player.set_camera_input_suppressed(
EMOTE_RADIAL_CAMERA_OWNER,
@ -713,6 +713,7 @@ func _input(event: InputEvent) -> void:
can_open and not _emote_radial_menu.is_open(),
):
var quick_is_open: bool = _quick_radial_menu.is_open()
_refresh_home_decor_toolbar_obscuring()
if quick_is_open != quick_was_open and _player != null:
_player.set_camera_input_suppressed(
QUICK_RADIAL_CAMERA_OWNER,
@ -755,6 +756,7 @@ func _close_radial_menus() -> void:
func _close_emote_radial_menu() -> void:
if _emote_radial_menu != null and _emote_radial_menu.is_open():
_emote_radial_menu.close_menu()
_refresh_home_decor_toolbar_obscuring()
if _player != null and is_instance_valid(_player):
_player.set_camera_input_suppressed(
EMOTE_RADIAL_CAMERA_OWNER,
@ -765,6 +767,7 @@ func _close_emote_radial_menu() -> void:
func _close_quick_radial_menu() -> void:
if _quick_radial_menu != null and _quick_radial_menu.is_open():
_quick_radial_menu.close_menu()
_refresh_home_decor_toolbar_obscuring()
if _player != null and is_instance_valid(_player):
_player.set_camera_input_suppressed(
QUICK_RADIAL_CAMERA_OWNER,
@ -798,6 +801,14 @@ func _character_call_yields_to_world_interaction(
_shop_interaction != null
and _shop_interaction.is_local_player_in_range()
)
or (
_decor_shop_interaction != null
and _decor_shop_interaction.is_local_player_in_range()
)
or (
_rv_upgrade_shop_interaction != null
and _rv_upgrade_shop_interaction.is_local_player_in_range()
)
or (
_storage_interaction != null
and _storage_interaction.is_local_player_in_range()
@ -806,6 +817,8 @@ func _character_call_yields_to_world_interaction(
and _fishing_spot != null
and _fishing_spot.can_open_fishing_shop()
and not _fishing_shop.visible
and not _decor_shop.visible
and not _rv_upgrade_shop.visible
)
@ -1544,12 +1557,19 @@ func _refresh_interaction_prompt_bindings() -> void:
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
if not _shop_npc_dialogue_active:
_shop_prompt_key_badge.position = Vector2.ZERO
_shop_prompt_key_badge.size = SHOP_PROMPT_COMPACT_SIZE
else:
var badge_width: float = maxf(
24.0,
ceilf(_shop_prompt_key.get_combined_minimum_size().x) + 12.0,
)
_shop_prompt_key_badge.position = Vector2(
_shop_prompt.size.x - badge_width + 6.0,
-8.0,
)
_shop_prompt_key_badge.size = Vector2(badge_width, 24.0)
var storage_width: float = maxf(
220.0,
ceilf(_storage_prompt_message.get_combined_minimum_size().x) + 24.0,
@ -1925,6 +1945,7 @@ func _refresh_gameplay_hud_visibility() -> void:
and not _storage_open
)
_gameplay_transient_hud.visible = show_world_hud
_gameplay_screen_effects.visible = show_world_hud
_experience_presentation.visible = (
_gameplay_ui_enabled and not _gameplay_hud_hidden
)
@ -2110,19 +2131,39 @@ func set_shop_prompt_visible(
func set_shop_npc_player_in_range(
in_range: bool,
dialogue_text: String = "",
source_id: int = 0,
dialogue_active: bool = false,
) -> void:
var source_changed: bool = source_id != _shop_npc_source_id
var next_dialogue_active: bool = in_range and dialogue_active
var dialogue_stage_changed: bool = (
next_dialogue_active != _shop_npc_dialogue_active
)
var dialogue_changed: bool = (
not dialogue_text.is_empty()
and _shop_prompt_message.text != dialogue_text
)
if source_changed or dialogue_changed or dialogue_stage_changed:
_cancel_shop_npc_speech()
_shop_npc_source_id = source_id
if dialogue_changed:
_shop_prompt_message.text = dialogue_text
_shop_prompt_message.visible_characters = -1
_shop_npc_spoken_for_current_visit = false
if _shop_npc_player_in_range == in_range and not dialogue_changed:
if source_changed or dialogue_stage_changed:
_shop_npc_spoken_for_current_visit = false
if (
_shop_npc_player_in_range == in_range
and not dialogue_changed
and not source_changed
and not dialogue_stage_changed
):
return
_shop_npc_player_in_range = in_range
if not in_range:
_shop_npc_dialogue_active = next_dialogue_active
_apply_shop_prompt_mode()
if not in_range or not _shop_npc_dialogue_active:
_cancel_shop_npc_speech()
_shop_npc_spoken_for_current_visit = false
return
_try_start_shop_npc_speech()
@ -2131,31 +2172,33 @@ func set_shop_npc_player_in_range(
func _try_start_shop_npc_speech() -> void:
if (
not _shop_npc_player_in_range
or not _shop_npc_dialogue_active
or _shop_npc_spoken_for_current_visit
or not _gameplay_ui_enabled
):
return
var now_msec: int = Time.get_ticks_msec()
if now_msec < _shop_npc_next_speech_msec:
return
_shop_npc_spoken_for_current_visit = true
_shop_npc_next_speech_msec = (
now_msec + SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS
)
_ensure_shop_animalese_voice()
TypewriterRevealType.start(
_shop_prompt_message,
SHOP_SPEECH_CHARACTERS_PER_SECOND,
)
_shop_animalese_voice.speak_text(
_shop_speech_tween = _shop_animalese_voice.speak_text(
_shop_prompt_message,
_shop_prompt_message.text,
"shopkeeper",
"shopkeeper:%d" % _shop_npc_source_id,
SHOP_ANIMALESE_VOICE_ID,
SHOP_SPEECH_CHARACTERS_PER_SECOND,
)
func _cancel_shop_npc_speech() -> void:
TypewriterRevealType.cancel(_shop_prompt_message)
if _shop_speech_tween != null and _shop_speech_tween.is_valid():
_shop_speech_tween.kill()
_shop_speech_tween = null
func _ensure_shop_animalese_voice() -> void:
if _shop_animalese_voice != null:
return
@ -2191,6 +2234,29 @@ func _apply_shop_prompt_style() -> void:
UtilityPageStyle.OCEAN_PANEL_MID,
0.96,
)
_apply_shop_prompt_mode()
func _apply_shop_prompt_mode() -> void:
if not is_node_ready():
return
var dialogue_mode: bool = _shop_npc_dialogue_active
var requested_size := (
SHOP_PROMPT_DIALOGUE_SIZE
if dialogue_mode
else SHOP_PROMPT_COMPACT_SIZE
)
_shop_prompt.custom_minimum_size = requested_size
_shop_prompt.size = requested_size
_shop_prompt_bubble.visible = dialogue_mode
_shop_prompt_message.visible = dialogue_mode
_shop_prompt_pointer.visible = dialogue_mode
var badge_style := (
_shop_prompt_key_badge.get_theme_stylebox("panel") as StyleBoxFlat
)
if badge_style != null:
badge_style.set_corner_radius_all(12 if dialogue_mode else 15)
_resize_interaction_prompts()
func _position_shop_prompt(world_anchor: Vector3) -> void:
@ -2219,7 +2285,7 @@ func _position_shop_prompt(world_anchor: Vector3) -> void:
):
_shop_prompt.hide()
return
var pointer_height := 10.0
var pointer_height := 10.0 if _shop_npc_dialogue_active else 6.0
var desired := screen_position - Vector2(
_shop_prompt.size.x * 0.5,
_shop_prompt.size.y + pointer_height,
@ -2236,14 +2302,15 @@ func _position_shop_prompt(world_anchor: Vector3) -> void:
ui_viewport_size.y - _shop_prompt.size.y - pointer_height - 8.0,
),
)
_shop_prompt_pointer.position = Vector2(
clampf(
screen_position.x - _shop_prompt.position.x,
20.0,
_shop_prompt.size.x - 20.0,
),
_shop_prompt.size.y - 1.0,
)
if _shop_npc_dialogue_active:
_shop_prompt_pointer.position = Vector2(
clampf(
screen_position.x - _shop_prompt.position.x,
20.0,
_shop_prompt.size.x - 20.0,
),
_shop_prompt.size.y - 1.0,
)
func get_screen_fade() -> ScreenFade:
@ -2831,7 +2898,6 @@ func _on_player_menu_visibility_changed(is_open: bool) -> void:
true,
UIMotion.PLAYER_MENU_ENTER_DURATION,
)
player_menu_backdrop_visibility_changed.emit(true)
else:
_player_menu_hotbar_visible = false
_hotbar_ui.set_presentation_visible(false, false)
@ -2862,13 +2928,6 @@ func _on_player_menu_exit_started() -> void:
true,
UIMotion.PLAYER_MENU_EXIT_DURATION,
)
player_menu_backdrop_visibility_changed.emit(false)
func _on_shop_exit_started() -> void:
if not _shop_open:
return
shop_backdrop_visibility_changed.emit(false)
func _on_shop_visibility_changed(is_open: bool) -> void:
@ -2876,8 +2935,6 @@ func _on_shop_visibility_changed(is_open: bool) -> void:
if is_open:
_close_radial_menus()
_refresh_surface_drawing_activation()
if is_open:
shop_backdrop_visibility_changed.emit(true)
if not is_open and _player_menu.is_shop_cooler_mounted():
_player_menu.unmount_shop_cooler()
_refresh_gameplay_hud_visibility()
@ -3049,6 +3106,7 @@ func _refresh_hotbar_visibility() -> void:
func _emit_interactive_pointer_ui_changed() -> void:
_refresh_active_bait_indicator_visibility()
_refresh_rv_storage_button_visibility()
_refresh_home_decor_toolbar_obscuring()
interactive_pointer_ui_changed.emit(
_system_menu_open
or _player_menu_open
@ -3058,6 +3116,26 @@ func _emit_interactive_pointer_ui_changed() -> void:
)
func _refresh_home_decor_toolbar_obscuring() -> void:
if _home_decor_toolbar == null:
return
_home_decor_toolbar.set_menu_obscured(
_system_menu_open
or _player_menu_open
or _shop_open
or _storage_open
or _chat_input_open
or (
_emote_radial_menu != null
and _emote_radial_menu.is_open()
)
or (
_quick_radial_menu != null
and _quick_radial_menu.is_open()
)
)
func _on_chat_text_entry_ownership_changed(active: bool) -> void:
_chat_input_open = active
if active:
@ -3080,4 +3158,5 @@ func _refresh_chat_availability() -> void:
)
_chat_ui.set_available(chat_available)
_chat_ui.set_hud_hidden(_gameplay_hud_hidden and not _chat_input_open)
_chat_ui.set_world_nameplates_hud_hidden(_gameplay_hud_hidden)
passive_pointer_ui_changed.emit(chat_available)

View file

@ -81,7 +81,7 @@ mouse_filter = 2
[node name="SurfaceDrawingToolbar" parent="UIRoot" instance=ExtResource("11_drawing_toolbar")]
unique_name_in_owner = true
[node name="GameplayTransientHUD" type="Control" parent="UIRoot/CanonicalStage"]
[node name="GameplayScreenEffects" type="Control" parent="UIRoot"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
@ -91,7 +91,7 @@ grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="FishingDangerVignette" type="ColorRect" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
[node name="FishingDangerVignette" type="ColorRect" parent="UIRoot/GameplayScreenEffects"]
unique_name_in_owner = true
visible = false
z_index = 50
@ -105,6 +105,16 @@ mouse_filter = 2
material = SubResource("ShaderMaterial_fishing_vignette")
color = Color(1, 1, 1, 1)
[node name="GameplayTransientHUD" type="Control" parent="UIRoot/CanonicalStage"]
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
mouse_filter = 2
[node name="ActiveBaitIndicator" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
unique_name_in_owner = true
z_index = 54

View file

@ -801,7 +801,10 @@ func _refresh_entries() -> void:
else "online"
)
_server_list.add_item("%s%s" % [
str(friend.get("display_name", "Player")), state,
str(friend.get(
"display_name",
NetworkProfilePreferences.DEFAULT_DISPLAY_NAME,
)), state,
])
return
if _saved_servers == null or _mode == Mode.DIRECT:
@ -1451,7 +1454,9 @@ func _format_friend_details(friend: Dictionary) -> String:
return "Select a friend."
var room := _selected_friend_room()
var lines: Array[String] = [
"Friend: %s" % str(friend.get("display_name", "Player")),
"Friend: %s" % str(friend.get(
"display_name", NetworkProfilePreferences.DEFAULT_DISPLAY_NAME
)),
"Status: %s" % (
"online" if bool(friend.get("online", false)) else "offline"
),

View file

@ -689,7 +689,10 @@ func _build_relationship_rows() -> void:
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
var fingerprint := str(record["fingerprint"])
label.text = "%s %s" % [
str(record.get("last_known_display_name", "Player")),
str(record.get(
"last_known_display_name",
NetworkProfilePreferences.DEFAULT_DISPLAY_NAME,
)),
"Blocked" if bool(record.get("blocked", false)) else "Muted",
]
_configure_identity_tooltip(
@ -733,7 +736,9 @@ func _build_friend_rows() -> void:
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 display_name := str(friend.get(
"display_name", NetworkProfilePreferences.DEFAULT_DISPLAY_NAME
))
var room: Dictionary = (
friend.get("room", {})
if typeof(friend.get("room", {})) == TYPE_DICTIONARY
@ -835,7 +840,10 @@ func _build_ban_rows() -> void:
label.clip_text = true
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.text = "%s banned %s" % [
str(record.get("last_known_display_name", "Player")),
str(record.get(
"last_known_display_name",
NetworkProfilePreferences.DEFAULT_DISPLAY_NAME,
)),
Time.get_date_string_from_unix_time(int(record.get("banned_unix", 0))),
]
_configure_identity_tooltip(

View file

@ -34,6 +34,7 @@ const VoiceProfilesType = preload(
"res://player/animalese_voice_profiles.gd"
)
const VOICE_CATEGORY_ID: String = "voice"
const NAME_TOOLTIP_BASE: String = "Shown to other players in multiplayer."
enum ControllerZone {
ACCOUNT,
@ -45,8 +46,10 @@ enum ControllerZone {
var _service: NetworkProfileService
var _experience: PlayerExperience
var _draft_name: String = ""
var _draft_title: String = NetworkProfilePreferences.DEFAULT_TITLE
var _draft_appearance: Dictionary = {}
var _persisted_name: String = ""
var _persisted_title: String = NetworkProfilePreferences.DEFAULT_TITLE
var _persisted_appearance: Dictionary = {}
var _draft_voice_id: String = VoiceProfilesType.DEFAULT_ID
var _persisted_voice_id: String = VoiceProfilesType.DEFAULT_ID
@ -61,6 +64,8 @@ var _dirty: bool = false
var _allow_duplicate: bool = false
var _name_edit: LineEdit
var _title_edit: LineEdit
var _name_label: Label
var _name_status: Label
var _suggestions: HBoxContainer
var _category_list: VBoxContainer
@ -215,8 +220,9 @@ func consume_escape() -> bool:
if _discard_confirmation.visible:
_close_discard_confirmation()
return true
if _name_edit.has_focus():
if _name_edit.has_focus() or _title_edit.has_focus():
_name_edit.release_focus()
_title_edit.release_focus()
return true
if _dirty:
_show_confirmation("discard")
@ -426,6 +432,18 @@ func _focus_controller_zone() -> void:
if not _profile_interactive:
return
var controls: Array[Control] = _active_controller_zone_controls()
# The account zone starts with a LineEdit in layout order. Do not activate
# text entry merely because the Profile page opened or controller focus
# returned from customization; keep the name field opt-in.
if (
_controller_zone == ControllerZone.ACCOUNT
and _customize_button in controls
and _customize_button.focus_mode != Control.FOCUS_NONE
and _customize_button.is_visible_in_tree()
and not _customize_button.disabled
):
_customize_button.grab_focus()
return
for control: Control in controls:
var button := control as BaseButton
if (
@ -496,6 +514,7 @@ func _account_controller_controls() -> Array[Control]:
var controls: Array[Control] = []
for control: Control in [
_name_edit,
_title_edit,
_customize_button,
_apply_button,
_revert_button,
@ -604,22 +623,26 @@ func _build_ui() -> void:
layout.add_child(account_row)
var account_stack := VBoxContainer.new()
account_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
account_stack.add_theme_constant_override("separation", 2)
account_stack.add_theme_constant_override("separation", 7)
account_row.add_child(account_stack)
var name_line := HBoxContainer.new()
name_line.name = "PlayerNameRow"
name_line.add_theme_constant_override("separation", 10)
account_stack.add_child(name_line)
var name_label := Label.new()
name_label.text = "player name"
name_label.custom_minimum_size.x = 98.0
name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
name_label.add_theme_color_override(
_name_label = Label.new()
_name_label.name = "PlayerNameLabel"
_name_label.text = "name"
_name_label.tooltip_text = NAME_TOOLTIP_BASE
_name_label.custom_minimum_size.x = 128.0
_name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_name_label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
name_line.add_child(name_label)
name_line.add_child(_name_label)
_name_edit = LineEdit.new()
_name_edit.name = "PlayerNameEdit"
_name_edit.max_length = NetworkProtocol.MAX_DISPLAY_NAME_LENGTH
_name_edit.placeholder_text = "Player"
_name_edit.placeholder_text = NetworkProfilePreferences.DEFAULT_DISPLAY_NAME
_name_edit.custom_minimum_size = Vector2(280, 34)
UtilityPageStyle.apply_ocean_line_edit(_name_edit)
_name_edit.add_theme_stylebox_override(
@ -629,23 +652,46 @@ func _build_ui() -> void:
)
_name_edit.text_changed.connect(_on_name_changed)
name_line.add_child(_name_edit)
var helper := Label.new()
helper.text = "Shown to other players in multiplayer."
helper.add_theme_font_size_override("font_size", 12)
helper.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
var title_line := HBoxContainer.new()
title_line.name = "PlayerTitleRow"
title_line.add_theme_constant_override("separation", 10)
account_stack.add_child(title_line)
var title_label := Label.new()
title_label.name = "PlayerTitleLabel"
title_label.text = "title"
title_label.tooltip_text = "Shown below your name to other players."
title_label.custom_minimum_size.x = 128.0
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
account_stack.add_child(helper)
title_line.add_child(title_label)
_title_edit = LineEdit.new()
_title_edit.name = "PlayerTitleEdit"
_title_edit.max_length = NetworkProtocol.MAX_PLAYER_TITLE_LENGTH
_title_edit.placeholder_text = NetworkProfilePreferences.DEFAULT_TITLE
_title_edit.custom_minimum_size = Vector2(280, 34)
UtilityPageStyle.apply_ocean_line_edit(_title_edit)
_title_edit.add_theme_stylebox_override(
"normal", UtilityPageStyle.ocean_button_style(
UtilityPageStyle.OCEAN_PANEL_MID
)
)
_title_edit.text_changed.connect(_on_title_changed)
title_line.add_child(_title_edit)
_name_status = Label.new()
_name_status.add_theme_font_size_override("font_size", 12)
_name_status.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
_name_status.name = "PlayerNameStatus"
_name_status.visible = false
account_stack.add_child(_name_status)
_suggestions = HBoxContainer.new()
_suggestions.add_theme_constant_override("separation", 6)
account_stack.add_child(_suggestions)
var account_info_stack := VBoxContainer.new()
account_info_stack.name = "ProfileIdentityStack"
account_info_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
account_info_stack.add_theme_constant_override("separation", 7)
account_row.add_child(account_info_stack)
var identity_value := Label.new()
identity_value.name = "IdentityFingerprint"
identity_value.text = "identity • stored on this device"
@ -656,11 +702,14 @@ func _build_ui() -> void:
identity_value.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
account_stack.add_child(identity_value)
identity_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
account_info_stack.add_child(identity_value)
var experience_row := HBoxContainer.new()
experience_row.name = "ProfileExperienceRow"
experience_row.alignment = BoxContainer.ALIGNMENT_END
experience_row.add_theme_constant_override("separation", 9)
account_stack.add_child(experience_row)
account_info_stack.add_child(experience_row)
_experience_level = Label.new()
_experience_level.custom_minimum_size.x = 64.0
_experience_level.add_theme_font_size_override("font_size", 14)
@ -695,7 +744,7 @@ func _build_ui() -> void:
actions.alignment = BoxContainer.ALIGNMENT_END
actions.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
actions.add_theme_constant_override("separation", 7)
account_row.add_child(actions)
account_info_stack.add_child(actions)
_customize_button = Button.new()
_customize_button.text = "customize"
_customize_button.custom_minimum_size.x = 96.0
@ -2127,10 +2176,16 @@ func _on_name_changed(value: String) -> void:
_refresh_actions()
_clear_suggestions()
if NetworkProfilePreferences.is_valid_display_name(value):
_name_status.text = "Checking this game…"
_set_name_status("Checking this game…")
_debounce.start()
else:
_name_status.text = "Use 124 plain-text characters."
_set_name_status("Use 124 plain-text characters.")
func _on_title_changed(value: String) -> void:
_draft_title = value
_dirty = _draft_differs()
_refresh_actions()
func _request_conflict_check() -> void:
@ -2145,9 +2200,9 @@ func _on_conflict_result(
) -> void:
_clear_suggestions()
if not has_conflict:
_name_status.text = "Name is available in this game."
_set_name_status("Name is available in this game.")
return
_name_status.text = "That name is already in use in this game."
_set_name_status("That name is already in use in this game.")
for suggestion: String in suggestion_names:
var button := Button.new()
button.text = suggestion
@ -2158,7 +2213,7 @@ func _on_conflict_result(
anyway.text = "use anyway"
anyway.pressed.connect(func() -> void:
_allow_duplicate = true
_name_status.text = "Duplicate name allowed for this apply."
_set_name_status("Duplicate name allowed for this apply.")
)
UtilityPageStyle.apply_compact_ocean_button(anyway)
_suggestions.add_child(anyway)
@ -2209,12 +2264,13 @@ func _apply() -> void:
_draft_speech_speed_id,
_draft_call_id,
_draft_sample_set_id,
_draft_title,
)
func _on_apply_finished(accepted: bool, message: String) -> void:
_apply_button.disabled = false
_name_status.text = message
_set_name_status(message)
if accepted:
_load_persisted()
@ -2223,6 +2279,7 @@ func _load_persisted() -> void:
if _service == null:
return
_persisted_name = _service.get_persisted_name()
_persisted_title = _service.get_persisted_title()
_persisted_appearance = _service.get_persisted_appearance()
_persisted_voice_id = _service.get_persisted_voice_id()
_persisted_speech_speed_id = (
@ -2231,6 +2288,7 @@ func _load_persisted() -> void:
_persisted_call_id = _service.get_persisted_call_id()
_persisted_sample_set_id = _service.get_persisted_sample_set_id()
_draft_name = _persisted_name
_draft_title = _persisted_title
_draft_appearance = _persisted_appearance.duplicate(true)
_draft_voice_id = _persisted_voice_id
_draft_speech_speed_id = _persisted_speech_speed_id
@ -2240,12 +2298,13 @@ func _load_persisted() -> void:
VoiceProfilesType.speed_for(_persisted_speech_speed_id)
)
_name_edit.text = _draft_name
_title_edit.text = _draft_title
_preview.apply_appearance_profile(_draft_appearance)
_appearance_preview_timer.stop()
_service.preview_appearance(_persisted_appearance)
_dirty = false
_allow_duplicate = false
_name_status.text = ""
_set_name_status("")
_clear_suggestions()
_refresh_options()
_refresh_actions()
@ -2288,6 +2347,8 @@ func _confirm_pending_action() -> void:
_draft_speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_ID
_draft_call_id = VoiceProfilesType.DEFAULT_CALL_ID
_draft_sample_set_id = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
_draft_title = NetworkProfilePreferences.DEFAULT_TITLE
_title_edit.text = _draft_title
_preview.apply_appearance_profile(_draft_appearance)
_queue_draft_appearance_preview()
_dirty = _draft_differs()
@ -2318,6 +2379,7 @@ func _publish_draft_appearance() -> void:
func _draft_differs() -> bool:
return (
_draft_name.strip_edges() != _persisted_name
or _draft_title.strip_edges() != _persisted_title
or _draft_appearance != _persisted_appearance
or _draft_voice_id != _persisted_voice_id
or _draft_speech_speed_id != _persisted_speech_speed_id
@ -2330,6 +2392,7 @@ func _refresh_actions() -> void:
_apply_button.disabled = (
not _dirty
or not NetworkProfilePreferences.is_valid_display_name(_draft_name)
or not NetworkProfilePreferences.is_valid_title(_draft_title)
)
_revert_button.disabled = not _dirty
@ -2339,3 +2402,12 @@ func _clear_suggestions() -> void:
_suggestions.remove_child(child)
child.queue_free()
_apply_controller_zone_focus()
func _set_name_status(message: String) -> void:
_name_status.text = message
_name_label.tooltip_text = (
NAME_TOOLTIP_BASE
if message.is_empty()
else "%s\n%s" % [NAME_TOOLTIP_BASE, message]
)

View file

@ -34,6 +34,7 @@ enum SeedMode {
@onready var _empty_slots_label: Label = %EmptySlotsLabel
@onready var _slot_name_edit: LineEdit = %SelectedSlotName
@onready var _slot_summary: RichTextLabel = %SlotSummary
@onready var _online_slot_button: CheckButton = %OnlineSlotButton
@onready var _play_button: Button = %PlaySlotButton
@onready var _rename_button: Button = %RenameSlotButton
@onready var _duplicate_button: Button = %DuplicateSlotButton
@ -75,6 +76,7 @@ func _ready() -> void:
_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)
_online_slot_button.toggled.connect(_on_online_slot_toggled)
_rename_button.pressed.connect(_rename_selected_slot)
_duplicate_button.pressed.connect(_prepare_duplicate)
_export_button.pressed.connect(_choose_export_path)
@ -285,9 +287,14 @@ func _refresh_slots() -> void:
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 ""
)
var markers: Array[String] = []
if bool(slot.get("active", false)):
markers.append("loaded")
if bool(slot.get("online", false)):
markers.append("online")
button.text = str(slot.get("display_name", "save"))
if not markers.is_empty():
button.text += " · " + " · ".join(markers)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
UtilityPageStyle.apply_ocean_button(button)
button.pressed.connect(_select_slot.bind(slot_id, false))
@ -321,10 +328,14 @@ func _refresh_selected_slot() -> void:
_slot_name_edit.text = ""
_slot_name_edit.editable = false
_slot_summary.text = "[center]create or import a save slot to begin.[/center]"
_online_slot_button.set_pressed_no_signal(false)
else:
_slot_name_edit.editable = true
_slot_name_edit.text = str(slot.get("display_name", "save"))
_slot_summary.text = _format_slot_summary(slot)
_online_slot_button.set_pressed_no_signal(
bool(slot.get("online", false))
)
_refresh_action_state()
@ -337,9 +348,9 @@ func _format_slot_summary(slot: Dictionary) -> String:
int(slot.get("last_played_at_unix", 0))
)
return (
"[font_size=24]level %d[/font_size]\n\n"
"[font_size=24]level %d[/font_size]\n"
+ "%d catches · %d discovered\n"
+ "%d fishcoins\n\n"
+ "%d fishcoins\n"
+ "%s · seed %d\n"
+ "last played %s"
) % [
@ -358,6 +369,7 @@ func _refresh_action_state() -> void:
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
_online_slot_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
@ -375,6 +387,21 @@ func _request_play() -> void:
play_requested.emit(_selected_slot_id)
func _on_online_slot_toggled(enabled: bool) -> void:
if _catalog == null or _selected_slot_id.is_empty():
return
if not enabled:
_online_slot_button.set_pressed_no_signal(
_catalog.get_online_slot_id() == _selected_slot_id
)
return
if not _catalog.mark_slot_for_online(_selected_slot_id):
_status.text = "the online progression could not be changed."
_online_slot_button.set_pressed_no_signal(false)
return
_status.text = "this progression will be used when joining online games."
func _rename_selected_slot() -> void:
if _catalog == null or _rename_button.disabled:
return
@ -644,8 +671,9 @@ func _configure_controller_focus() -> void:
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, _play_button, _slot_name_edit, _rename_button)
_set_neighbors(_slot_name_edit, first, _slot_name_edit, _saves_tab, _online_slot_button)
_set_neighbors(_online_slot_button, _import_button, _online_slot_button, _slot_name_edit, _play_button)
_set_neighbors(_play_button, _import_button, _play_button, _online_slot_button, _rename_button)
_set_neighbors(_rename_button, _import_button, _duplicate_button, _play_button, _export_button)
_set_neighbors(_duplicate_button, _rename_button, _duplicate_button, _play_button, _delete_button)
_set_neighbors(_export_button, _import_button, _delete_button, _rename_button, _back_button)

View file

@ -140,7 +140,7 @@ 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
theme_override_constants/separation = 8
[node name="Title" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn"]
layout_mode = 2
@ -165,6 +165,14 @@ scroll_active = false
autowrap_mode = 2
vertical_alignment = 1
[node name="OnlineSlotButton" type="CheckButton" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 44)
layout_mode = 2
focus_mode = 2
text = "use this save online"
tooltip_text = "Use this save when joining an online room"
[node name="Actions" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/SavesPage/DetailsColumn"]
unique_name_in_owner = true
layout_mode = 2
@ -330,7 +338,7 @@ text = "create and play"
[node name="FooterGroup" type="MarginContainer" parent="MainPanel/OuterMargin/Layout"]
layout_mode = 2
theme_override_constants/margin_top = 34
theme_override_constants/margin_top = 32
[node name="FooterLayout" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/FooterGroup"]
layout_mode = 2

View file

@ -628,6 +628,7 @@ func _prepare_awaiting_start_input() -> void:
_continue_stats_drawer.hide()
_continue_stats_drawer.modulate.a = 0.0
_continue_stats_drawer.scale = Vector2(1.0, 0.001)
_version_row.show()
_main_content.show()
_start_prompt_center.show()
_start_prompt_center.modulate.a = 1.0
@ -927,6 +928,7 @@ func _on_continue_pressed() -> void:
_release_primary_menu_focus()
_presentation_center.hide()
_start_prompt_center.hide()
_version_row.hide()
_join_game_page.close_page()
_credits_page.close_page()
_new_game_setup_page.close_page()
@ -935,6 +937,7 @@ func _on_continue_pressed() -> void:
func _close_save_slots() -> void:
_save_slots_page.close_page()
_version_row.show()
_presentation_center.show()
_button_center.show()
_start_prompt_center.hide()

View file

@ -4,6 +4,7 @@ extends RefCounted
const DEFAULT_CHARACTERS_PER_SECOND: float = 28.0
const MIN_CHARACTERS_PER_SECOND: float = 12.0
const MAX_CHARACTERS_PER_SECOND: float = 60.0
const ACTIVE_TWEEN_META: StringName = &"typewriter_reveal_active_tween"
static var _characters_per_second: float = DEFAULT_CHARACTERS_PER_SECOND
@ -26,6 +27,7 @@ static func start(
) -> float:
if label == null:
return 0.0
cancel(label, false)
var character_count: int = label.get_total_character_count()
if character_count <= 0:
label.visible_characters = -1
@ -38,10 +40,39 @@ static func start(
)
var duration := float(character_count) / resolved_characters_per_second
var reveal_tween := label.create_tween()
label.set_meta(ACTIVE_TWEEN_META, reveal_tween)
reveal_tween.tween_property(
label,
"visible_characters",
character_count,
duration,
).from(0).set_trans(Tween.TRANS_LINEAR)
reveal_tween.finished.connect(
_on_reveal_finished.bind(label, reveal_tween),
CONNECT_ONE_SHOT,
)
return duration
static func cancel(label: Label, reveal_all: bool = true) -> void:
if label == null:
return
var reveal_tween: Tween
if label.has_meta(ACTIVE_TWEEN_META):
reveal_tween = label.get_meta(ACTIVE_TWEEN_META) as Tween
if reveal_tween != null and reveal_tween.is_valid():
reveal_tween.kill()
if label.has_meta(ACTIVE_TWEEN_META):
label.remove_meta(ACTIVE_TWEEN_META)
if reveal_all:
label.visible_characters = -1
static func _on_reveal_finished(label: Label, reveal_tween: Tween) -> void:
if label == null or not is_instance_valid(label):
return
if (
label.has_meta(ACTIVE_TWEEN_META)
and label.get_meta(ACTIVE_TWEEN_META) == reveal_tween
):
label.remove_meta(ACTIVE_TWEEN_META)