Improve chat presentation and draft handling
This commit is contained in:
parent
985fe1c28f
commit
4985cc3374
7 changed files with 354 additions and 59 deletions
|
|
@ -392,6 +392,7 @@ func _initialize_after_data_root() -> void:
|
||||||
_asset_reservations,
|
_asset_reservations,
|
||||||
_network_profile_service,
|
_network_profile_service,
|
||||||
_network_player_list,
|
_network_player_list,
|
||||||
|
_settings_manager,
|
||||||
)
|
)
|
||||||
_game_ui.setup_data_and_identity(
|
_game_ui.setup_data_and_identity(
|
||||||
_data_root,
|
_data_root,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ const WINDOW_COUNT: int = 5
|
||||||
const WINDOW_SECONDS: float = 10.0
|
const WINDOW_SECONDS: float = 10.0
|
||||||
|
|
||||||
signal message_received(message: Dictionary)
|
signal message_received(message: Dictionary)
|
||||||
|
signal local_message_confirmed(message: Dictionary)
|
||||||
signal send_rejected(message: String)
|
signal send_rejected(message: String)
|
||||||
signal history_replaced(messages: Array[Dictionary])
|
signal history_replaced(messages: Array[Dictionary])
|
||||||
|
|
||||||
|
|
@ -222,6 +223,12 @@ func _apply_message(data: Dictionary) -> void:
|
||||||
_history.pop_front()
|
_history.pop_front()
|
||||||
if _message_is_visible(stored_message):
|
if _message_is_visible(stored_message):
|
||||||
message_received.emit(stored_message.duplicate(true))
|
message_received.emit(stored_message.duplicate(true))
|
||||||
|
if (
|
||||||
|
kind == NetworkChatProtocol.Kind.PLAYER
|
||||||
|
and int(stored_message["sender_peer_id"])
|
||||||
|
== _session.get_local_peer_id()
|
||||||
|
):
|
||||||
|
local_message_confirmed.emit(stored_message.duplicate(true))
|
||||||
|
|
||||||
|
|
||||||
func _send_rejection(peer_id: int, message: String) -> void:
|
func _send_rejection(peer_id: int, message: String) -> void:
|
||||||
|
|
|
||||||
|
|
@ -579,6 +579,23 @@ func get_body_center_position() -> Vector3:
|
||||||
return global_position + Vector3.UP * body_center_height
|
return global_position + Vector3.UP * body_center_height
|
||||||
|
|
||||||
|
|
||||||
|
func get_chat_anchor_position() -> Vector3:
|
||||||
|
var highest_y := -INF
|
||||||
|
for node: Node in _visuals.find_children("*", "MeshInstance3D", true, false):
|
||||||
|
var mesh_instance := node as MeshInstance3D
|
||||||
|
if mesh_instance == null or not mesh_instance.is_visible_in_tree():
|
||||||
|
continue
|
||||||
|
var bounds := mesh_instance.get_aabb()
|
||||||
|
for endpoint_index: int in range(8):
|
||||||
|
var world_point := (
|
||||||
|
mesh_instance.global_transform * bounds.get_endpoint(endpoint_index)
|
||||||
|
)
|
||||||
|
highest_y = maxf(highest_y, world_point.y)
|
||||||
|
if not is_finite(highest_y):
|
||||||
|
highest_y = get_body_center_position().y + body_center_height
|
||||||
|
return Vector3(global_position.x, highest_y + 0.22, global_position.z)
|
||||||
|
|
||||||
|
|
||||||
func get_body_center_height() -> float:
|
func get_body_center_height() -> float:
|
||||||
return body_center_height
|
return body_center_height
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const DEFAULT_WORLD_PIXEL_SIZE: int = 3
|
||||||
const MIN_UI_PIXEL_SIZE: int = 1
|
const MIN_UI_PIXEL_SIZE: int = 1
|
||||||
const MAX_UI_PIXEL_SIZE: int = 5
|
const MAX_UI_PIXEL_SIZE: int = 5
|
||||||
const DEFAULT_UI_PIXEL_SIZE: int = 3
|
const DEFAULT_UI_PIXEL_SIZE: int = 3
|
||||||
|
const MAX_CHAT_DRAFT_CHARACTERS: int = 280
|
||||||
const COMPACT_DISPLAY_HEIGHT: int = 560
|
const COMPACT_DISPLAY_HEIGHT: int = 560
|
||||||
const WORLD_DESKTOP_GRID_HEIGHTS: Array[int] = [0, 540, 360, 216, 96]
|
const WORLD_DESKTOP_GRID_HEIGHTS: Array[int] = [0, 540, 360, 216, 96]
|
||||||
const WORLD_COMPACT_GRID_HEIGHTS: Array[int] = [0, 360, 240, 144, 72]
|
const WORLD_COMPACT_GRID_HEIGHTS: Array[int] = [0, 360, 240, 144, 72]
|
||||||
|
|
@ -25,6 +26,8 @@ const UI_COMPACT_RENDER_HEIGHTS: Array[int] = [0, 408, 336, 264, 192]
|
||||||
@export_range(0.001, 0.012, 0.0005) var mouse_camera_sensitivity: float = 0.005
|
@export_range(0.001, 0.012, 0.0005) var mouse_camera_sensitivity: float = 0.005
|
||||||
@export_range(0.5, 5.0, 0.1) var controller_camera_sensitivity: float = 2.5
|
@export_range(0.5, 5.0, 0.1) var controller_camera_sensitivity: float = 2.5
|
||||||
@export var invert_camera_y: bool = false
|
@export var invert_camera_y: bool = false
|
||||||
|
@export var chat_draft: String = ""
|
||||||
|
@export var chat_collapsed: bool = false
|
||||||
@export_range(1, 5, 1) var world_pixel_size: int = DEFAULT_WORLD_PIXEL_SIZE
|
@export_range(1, 5, 1) var world_pixel_size: int = DEFAULT_WORLD_PIXEL_SIZE
|
||||||
@export_range(1, 5, 1) var ui_pixel_size: int = DEFAULT_UI_PIXEL_SIZE
|
@export_range(1, 5, 1) var ui_pixel_size: int = DEFAULT_UI_PIXEL_SIZE
|
||||||
|
|
||||||
|
|
@ -40,6 +43,7 @@ func is_valid() -> bool:
|
||||||
and is_finite(controller_camera_sensitivity)
|
and is_finite(controller_camera_sensitivity)
|
||||||
and controller_camera_sensitivity >= MIN_CONTROLLER_SENSITIVITY
|
and controller_camera_sensitivity >= MIN_CONTROLLER_SENSITIVITY
|
||||||
and controller_camera_sensitivity <= MAX_CONTROLLER_SENSITIVITY
|
and controller_camera_sensitivity <= MAX_CONTROLLER_SENSITIVITY
|
||||||
|
and chat_draft.length() <= MAX_CHAT_DRAFT_CHARACTERS
|
||||||
and world_pixel_size >= MIN_WORLD_PIXEL_SIZE
|
and world_pixel_size >= MIN_WORLD_PIXEL_SIZE
|
||||||
and world_pixel_size <= MAX_WORLD_PIXEL_SIZE
|
and world_pixel_size <= MAX_WORLD_PIXEL_SIZE
|
||||||
and ui_pixel_size >= MIN_UI_PIXEL_SIZE
|
and ui_pixel_size >= MIN_UI_PIXEL_SIZE
|
||||||
|
|
@ -55,6 +59,8 @@ func copy() -> PlayerSettings:
|
||||||
result.mouse_camera_sensitivity = mouse_camera_sensitivity
|
result.mouse_camera_sensitivity = mouse_camera_sensitivity
|
||||||
result.controller_camera_sensitivity = controller_camera_sensitivity
|
result.controller_camera_sensitivity = controller_camera_sensitivity
|
||||||
result.invert_camera_y = invert_camera_y
|
result.invert_camera_y = invert_camera_y
|
||||||
|
result.chat_draft = chat_draft
|
||||||
|
result.chat_collapsed = chat_collapsed
|
||||||
result.world_pixel_size = world_pixel_size
|
result.world_pixel_size = world_pixel_size
|
||||||
result.ui_pixel_size = ui_pixel_size
|
result.ui_pixel_size = ui_pixel_size
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,14 @@ func load_settings() -> bool:
|
||||||
accessibility.has("use_readable_interface_font")
|
accessibility.has("use_readable_interface_font")
|
||||||
and typeof(accessibility["use_readable_interface_font"]) != TYPE_BOOL
|
and typeof(accessibility["use_readable_interface_font"]) != TYPE_BOOL
|
||||||
)
|
)
|
||||||
|
or (
|
||||||
|
presentation.has("chat_draft")
|
||||||
|
and typeof(presentation["chat_draft"]) != TYPE_STRING
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
presentation.has("chat_collapsed")
|
||||||
|
and typeof(presentation["chat_collapsed"]) != TYPE_BOOL
|
||||||
|
)
|
||||||
):
|
):
|
||||||
return _use_defaults_after_corruption("Player settings values are invalid.")
|
return _use_defaults_after_corruption("Player settings values are invalid.")
|
||||||
var loaded := PlayerSettings.new()
|
var loaded := PlayerSettings.new()
|
||||||
|
|
@ -87,6 +95,10 @@ func load_settings() -> bool:
|
||||||
PlayerSettings.MAX_UI_PIXEL_SIZE,
|
PlayerSettings.MAX_UI_PIXEL_SIZE,
|
||||||
PlayerSettings.DEFAULT_UI_PIXEL_SIZE
|
PlayerSettings.DEFAULT_UI_PIXEL_SIZE
|
||||||
)
|
)
|
||||||
|
loaded.chat_draft = str(presentation.get("chat_draft", "")).left(
|
||||||
|
PlayerSettings.MAX_CHAT_DRAFT_CHARACTERS
|
||||||
|
)
|
||||||
|
loaded.chat_collapsed = bool(presentation.get("chat_collapsed", false))
|
||||||
if not loaded.is_valid():
|
if not loaded.is_valid():
|
||||||
return _use_defaults_after_corruption("Player settings values are invalid.")
|
return _use_defaults_after_corruption("Player settings values are invalid.")
|
||||||
current_settings = loaded
|
current_settings = loaded
|
||||||
|
|
@ -123,6 +135,22 @@ func apply_pixelation(world_pixel_size: int, ui_pixel_size: int) -> bool:
|
||||||
return apply_settings(edited)
|
return apply_settings(edited)
|
||||||
|
|
||||||
|
|
||||||
|
func update_chat_preferences(draft: String, collapsed: bool) -> void:
|
||||||
|
var clean_draft := draft.left(PlayerSettings.MAX_CHAT_DRAFT_CHARACTERS)
|
||||||
|
if (
|
||||||
|
current_settings.chat_draft == clean_draft
|
||||||
|
and current_settings.chat_collapsed == collapsed
|
||||||
|
):
|
||||||
|
return
|
||||||
|
current_settings.chat_draft = clean_draft
|
||||||
|
current_settings.chat_collapsed = collapsed
|
||||||
|
_is_dirty = true
|
||||||
|
|
||||||
|
|
||||||
|
func save_chat_preferences() -> bool:
|
||||||
|
return save_if_dirty()
|
||||||
|
|
||||||
|
|
||||||
func save_now() -> bool:
|
func save_now() -> bool:
|
||||||
if current_settings == null or not current_settings.is_valid():
|
if current_settings == null or not current_settings.is_valid():
|
||||||
return false
|
return false
|
||||||
|
|
@ -143,8 +171,12 @@ func save_now() -> bool:
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"world_pixel_size": current_settings.world_pixel_size,
|
"world_pixel_size": current_settings.world_pixel_size,
|
||||||
"ui_pixel_size": current_settings.ui_pixel_size,
|
"ui_pixel_size": current_settings.ui_pixel_size,
|
||||||
|
"chat_draft": current_settings.chat_draft,
|
||||||
|
"chat_collapsed": current_settings.chat_collapsed,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if current_settings.chat_draft.is_empty():
|
||||||
|
(data["presentation"] as Dictionary).erase("chat_draft")
|
||||||
var file := FileAccess.open(TEMP_PATH, FileAccess.WRITE)
|
var file := FileAccess.open(TEMP_PATH, FileAccess.WRITE)
|
||||||
if file == null:
|
if file == null:
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
340
ui/chat_ui.gd
340
ui/chat_ui.gd
|
|
@ -2,23 +2,39 @@ class_name ChatUI
|
||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
const INPUT_OWNER: StringName = &"chat"
|
const INPUT_OWNER: StringName = &"chat"
|
||||||
const COMPACT_SECONDS: float = 8.0
|
const RECENT_SECONDS: float = 8.0
|
||||||
const SPEECH_SECONDS: float = 6.0
|
const SPEECH_SECONDS: float = 6.0
|
||||||
|
const DRAFT_SAVE_DELAY: float = 0.4
|
||||||
|
const IDLE_ALPHA: float = 0.58
|
||||||
|
const PANEL_POSITION := Vector2(30, 376)
|
||||||
|
const PANEL_SIZE := Vector2(390, 250)
|
||||||
|
|
||||||
var _service: NetworkChatService
|
var _service: NetworkChatService
|
||||||
var _session: NetworkSession
|
var _session: NetworkSession
|
||||||
var _spawn: PlayerSpawnService
|
var _spawn: PlayerSpawnService
|
||||||
var _player: Player
|
var _player: Player
|
||||||
var _fishing_spot: FishingSpot
|
var _fishing_spot: FishingSpot
|
||||||
|
var _settings: PlayerSettingsManager
|
||||||
var _history: Label
|
var _history: Label
|
||||||
var _entry: LineEdit
|
var _entry: LineEdit
|
||||||
var _chat_button: Button
|
var _status: Label
|
||||||
var _panel: PanelContainer
|
var _panel: PanelContainer
|
||||||
|
var _collapse_button: Button
|
||||||
|
var _unread_indicator: Label
|
||||||
|
var _hint: Label
|
||||||
var _speech_layer: Control
|
var _speech_layer: Control
|
||||||
var _speech: Dictionary[int, Dictionary] = {}
|
var _speech: Dictionary[int, Dictionary] = {}
|
||||||
|
var _draft_save_timer: Timer
|
||||||
|
var _opacity_tween: Tween
|
||||||
var _opened: bool = false
|
var _opened: bool = false
|
||||||
var _available: bool = false
|
var _available: bool = false
|
||||||
|
var _collapsed: bool = false
|
||||||
|
var _panel_hovered: bool = false
|
||||||
|
var _collapsed_has_unread: bool = false
|
||||||
var _last_message_time: float = -INF
|
var _last_message_time: float = -INF
|
||||||
|
var _target_alpha: float = -1.0
|
||||||
|
var _send_pending: bool = false
|
||||||
|
var _pending_send_body: String = ""
|
||||||
var _prior_movement: bool = true
|
var _prior_movement: bool = true
|
||||||
var _prior_camera: bool = true
|
var _prior_camera: bool = true
|
||||||
|
|
||||||
|
|
@ -27,6 +43,7 @@ func _ready() -> void:
|
||||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
_build_ui()
|
_build_ui()
|
||||||
|
set_process(true)
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(
|
||||||
|
|
@ -35,16 +52,22 @@ func setup(
|
||||||
spawn: PlayerSpawnService,
|
spawn: PlayerSpawnService,
|
||||||
player: Player,
|
player: Player,
|
||||||
fishing_spot: FishingSpot,
|
fishing_spot: FishingSpot,
|
||||||
|
settings: PlayerSettingsManager,
|
||||||
) -> void:
|
) -> void:
|
||||||
_service = service
|
_service = service
|
||||||
_session = session
|
_session = session
|
||||||
_spawn = spawn
|
_spawn = spawn
|
||||||
_player = player
|
_player = player
|
||||||
_fishing_spot = fishing_spot
|
_fishing_spot = fishing_spot
|
||||||
|
_settings = settings
|
||||||
_service.message_received.connect(_on_message)
|
_service.message_received.connect(_on_message)
|
||||||
|
_service.local_message_confirmed.connect(_on_local_message_confirmed)
|
||||||
_service.history_replaced.connect(_on_history)
|
_service.history_replaced.connect(_on_history)
|
||||||
_service.send_rejected.connect(_on_rejected)
|
_service.send_rejected.connect(_on_rejected)
|
||||||
_session.peer_removed.connect(_on_peer_removed)
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
|
_entry.text = _settings.current_settings.chat_draft
|
||||||
|
_entry.caret_column = _entry.text.length()
|
||||||
|
_set_collapsed(_settings.current_settings.chat_collapsed, false)
|
||||||
_refresh_history()
|
_refresh_history()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,26 +77,32 @@ func open_chat() -> void:
|
||||||
or not _session.is_gameplay_session_active()
|
or not _session.is_gameplay_session_active()
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
_set_collapsed(false)
|
||||||
_opened = true
|
_opened = true
|
||||||
_prior_movement = _player.is_movement_enabled()
|
_prior_movement = _player.is_movement_enabled()
|
||||||
_prior_camera = _player.is_camera_input_enabled()
|
_prior_camera = _player.is_camera_input_enabled()
|
||||||
_player.set_movement_enabled(false)
|
_player.set_movement_enabled(false)
|
||||||
_player.set_camera_input_enabled(false)
|
_player.set_camera_input_enabled(false)
|
||||||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
||||||
# Send the host a zeroed frame immediately so it cannot keep applying the
|
# Clear the last authoritative input immediately. Merely disabling local
|
||||||
# last movement state while this LineEdit owns keyboard input.
|
# prediction would let a remote host continue the last held movement.
|
||||||
_session.submit_neutral_local_movement()
|
_session.submit_neutral_local_movement()
|
||||||
_panel.show()
|
|
||||||
_entry.show()
|
_entry.show()
|
||||||
_entry.grab_focus()
|
_entry.grab_focus()
|
||||||
|
_hint.hide()
|
||||||
|
_update_panel_opacity(true)
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
|
||||||
|
|
||||||
func set_available(value: bool) -> void:
|
func set_available(value: bool) -> void:
|
||||||
_available = value
|
_available = value
|
||||||
_chat_button.visible = value
|
|
||||||
if not value:
|
if not value:
|
||||||
|
_send_pending = false
|
||||||
|
_pending_send_body = ""
|
||||||
|
_entry.editable = true
|
||||||
close_chat()
|
close_chat()
|
||||||
|
_flush_draft()
|
||||||
|
_refresh_visibility()
|
||||||
|
|
||||||
|
|
||||||
func close_chat() -> void:
|
func close_chat() -> void:
|
||||||
|
|
@ -85,6 +114,7 @@ func close_chat() -> void:
|
||||||
_player.set_movement_enabled(_prior_movement)
|
_player.set_movement_enabled(_prior_movement)
|
||||||
_player.set_camera_input_enabled(_prior_camera)
|
_player.set_camera_input_enabled(_prior_camera)
|
||||||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
|
||||||
|
_flush_draft()
|
||||||
_refresh_visibility()
|
_refresh_visibility()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -109,99 +139,203 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
_refresh_visibility()
|
_refresh_visibility()
|
||||||
var now := Time.get_ticks_msec() / 1000.0
|
_update_panel_opacity()
|
||||||
for peer_id: int in _speech.keys():
|
_update_speech()
|
||||||
var state: Dictionary = _speech[peer_id]
|
|
||||||
var label := state.get("label") as Label
|
|
||||||
if label == null or now >= float(state.get("expires", 0.0)):
|
func _exit_tree() -> void:
|
||||||
if label != null:
|
_flush_draft()
|
||||||
label.queue_free()
|
|
||||||
_speech.erase(peer_id)
|
|
||||||
continue
|
|
||||||
var avatar := _spawn.get_avatar(peer_id)
|
|
||||||
var camera := _player.get_gameplay_camera()
|
|
||||||
if avatar == null or camera == null:
|
|
||||||
label.hide()
|
|
||||||
continue
|
|
||||||
var world_position := avatar.get_body_center_position() + Vector3.UP * 1.2
|
|
||||||
if camera.is_position_behind(world_position):
|
|
||||||
label.hide()
|
|
||||||
continue
|
|
||||||
var screen_position := camera.unproject_position(world_position)
|
|
||||||
label.position = screen_position - Vector2(label.size.x * 0.5, 42.0)
|
|
||||||
label.show()
|
|
||||||
|
|
||||||
|
|
||||||
func _build_ui() -> void:
|
func _build_ui() -> void:
|
||||||
_panel = PanelContainer.new()
|
_panel = PanelContainer.new()
|
||||||
_panel.position = Vector2(18, 390)
|
_panel.position = PANEL_POSITION
|
||||||
_panel.size = Vector2(390, 240)
|
_panel.size = PANEL_SIZE
|
||||||
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
_panel.add_theme_stylebox_override("panel", _chat_panel_style())
|
||||||
|
_panel.mouse_entered.connect(func() -> void:
|
||||||
|
_panel_hovered = true
|
||||||
|
_update_panel_opacity(true)
|
||||||
|
)
|
||||||
|
_panel.mouse_exited.connect(func() -> void:
|
||||||
|
_panel_hovered = false
|
||||||
|
_update_panel_opacity(true)
|
||||||
|
)
|
||||||
add_child(_panel)
|
add_child(_panel)
|
||||||
var box := VBoxContainer.new()
|
var box := VBoxContainer.new()
|
||||||
box.add_theme_constant_override("separation", 6)
|
box.add_theme_constant_override("separation", 6)
|
||||||
_panel.add_child(box)
|
_panel.add_child(box)
|
||||||
_history = Label.new()
|
_history = Label.new()
|
||||||
_history.custom_minimum_size = Vector2(360, 150)
|
_history.custom_minimum_size = Vector2(360, 158)
|
||||||
_history.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
_history.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
_history.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
_history.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||||||
_history.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
_history.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_history.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
|
||||||
|
_history.add_theme_color_override("font_color", UtilityPageStyle.LIGHT_TEXT)
|
||||||
box.add_child(_history)
|
box.add_child(_history)
|
||||||
|
_status = Label.new()
|
||||||
|
_status.custom_minimum_size = Vector2(360, 20)
|
||||||
|
_status.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_status.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
|
||||||
|
_status.add_theme_color_override("font_color", Color("ffd6a1"))
|
||||||
|
box.add_child(_status)
|
||||||
_entry = LineEdit.new()
|
_entry = LineEdit.new()
|
||||||
_entry.placeholder_text = "type a message…"
|
_entry.placeholder_text = "type a message…"
|
||||||
_entry.max_length = NetworkChatProtocol.MAX_VISIBLE_CHARACTERS
|
_entry.max_length = NetworkChatProtocol.MAX_VISIBLE_CHARACTERS
|
||||||
_entry.text_submitted.connect(func(_value: String) -> void: _send())
|
_entry.text_submitted.connect(func(_value: String) -> void: _send())
|
||||||
_entry.hide()
|
_entry.text_changed.connect(_on_draft_changed)
|
||||||
|
UtilityPageStyle.apply_line_edit(_entry)
|
||||||
box.add_child(_entry)
|
box.add_child(_entry)
|
||||||
_chat_button = Button.new()
|
_entry.hide()
|
||||||
_chat_button.text = "chat"
|
_collapse_button = Button.new()
|
||||||
_chat_button.position = Vector2(18, 570)
|
_collapse_button.position = Vector2(2, PANEL_POSITION.y + 101)
|
||||||
_chat_button.size = Vector2(76, 54)
|
_collapse_button.size = Vector2(34, 48)
|
||||||
_chat_button.pressed.connect(open_chat)
|
_collapse_button.tooltip_text = "Collapse chat"
|
||||||
add_child(_chat_button)
|
_collapse_button.pressed.connect(func() -> void:
|
||||||
|
_set_collapsed(not _collapsed)
|
||||||
|
)
|
||||||
|
_collapse_button.focus_entered.connect(func() -> void:
|
||||||
|
_update_panel_opacity(true)
|
||||||
|
)
|
||||||
|
UtilityPageStyle.apply_button(_collapse_button)
|
||||||
|
add_child(_collapse_button)
|
||||||
|
_unread_indicator = Label.new()
|
||||||
|
_unread_indicator.text = "•"
|
||||||
|
_unread_indicator.position = Vector2(8, PANEL_POSITION.y + 86)
|
||||||
|
_unread_indicator.size = Vector2(22, 20)
|
||||||
|
_unread_indicator.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_unread_indicator.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_unread_indicator.add_theme_font_override(
|
||||||
|
"font", UtilityPageStyle.TuffyFont
|
||||||
|
)
|
||||||
|
_unread_indicator.add_theme_font_size_override("font_size", 20)
|
||||||
|
_unread_indicator.add_theme_color_override("font_color", Color("ffe08a"))
|
||||||
|
add_child(_unread_indicator)
|
||||||
|
_hint = Label.new()
|
||||||
|
_hint.text = "Press Enter to chat"
|
||||||
|
_hint.position = Vector2(30, 655)
|
||||||
|
_hint.size = Vector2(260, 28)
|
||||||
|
_hint.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_hint.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
|
||||||
|
_hint.add_theme_color_override(
|
||||||
|
"font_color", Color(0.94, 0.91, 0.80, 0.72)
|
||||||
|
)
|
||||||
|
_hint.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8))
|
||||||
|
_hint.add_theme_constant_override("shadow_offset_x", 1)
|
||||||
|
_hint.add_theme_constant_override("shadow_offset_y", 2)
|
||||||
|
add_child(_hint)
|
||||||
_speech_layer = Control.new()
|
_speech_layer = Control.new()
|
||||||
_speech_layer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
_speech_layer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
_speech_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
_speech_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
add_child(_speech_layer)
|
add_child(_speech_layer)
|
||||||
|
_draft_save_timer = Timer.new()
|
||||||
|
_draft_save_timer.one_shot = true
|
||||||
|
_draft_save_timer.wait_time = DRAFT_SAVE_DELAY
|
||||||
|
_draft_save_timer.timeout.connect(_flush_draft)
|
||||||
|
add_child(_draft_save_timer)
|
||||||
_panel.hide()
|
_panel.hide()
|
||||||
|
_collapse_button.hide()
|
||||||
|
_unread_indicator.hide()
|
||||||
|
_hint.hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _chat_panel_style() -> StyleBoxFlat:
|
||||||
|
var style := UtilityPageStyle.button_style(Color(0.025, 0.13, 0.19, 0.94))
|
||||||
|
style.border_color = Color(0.75, 0.84, 0.74, 0.66)
|
||||||
|
style.set_corner_radius_all(10)
|
||||||
|
style.content_margin_left = 12
|
||||||
|
style.content_margin_right = 12
|
||||||
|
style.content_margin_top = 10
|
||||||
|
style.content_margin_bottom = 10
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _speech_panel_style() -> StyleBoxFlat:
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(UtilityPageStyle.PAPER_ALT, 0.96)
|
||||||
|
style.border_color = Color(UtilityPageStyle.BORDER, 0.88)
|
||||||
|
style.set_border_width_all(2)
|
||||||
|
style.set_corner_radius_all(12)
|
||||||
|
style.content_margin_left = 10
|
||||||
|
style.content_margin_right = 10
|
||||||
|
style.content_margin_top = 6
|
||||||
|
style.content_margin_bottom = 6
|
||||||
|
style.shadow_color = Color(0, 0, 0, 0.32)
|
||||||
|
style.shadow_size = 4
|
||||||
|
style.shadow_offset = Vector2(2, 3)
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
func _send() -> void:
|
func _send() -> void:
|
||||||
|
if _send_pending:
|
||||||
|
return
|
||||||
var body := _entry.text
|
var body := _entry.text
|
||||||
if _service.send_local_message(body):
|
_send_pending = true
|
||||||
|
_pending_send_body = NetworkChatProtocol.sanitize_body(body)
|
||||||
|
_entry.editable = false
|
||||||
|
if not _service.send_local_message(body):
|
||||||
|
_send_pending = false
|
||||||
|
_pending_send_body = ""
|
||||||
|
_entry.editable = true
|
||||||
|
elif _send_pending:
|
||||||
|
_status.text = "Sending…"
|
||||||
|
|
||||||
|
|
||||||
|
func _on_local_message_confirmed(message: Dictionary) -> void:
|
||||||
|
if (
|
||||||
|
not _send_pending
|
||||||
|
or str(message.get("body", "")) != _pending_send_body
|
||||||
|
):
|
||||||
|
return
|
||||||
|
_send_pending = false
|
||||||
|
_pending_send_body = ""
|
||||||
|
_entry.editable = true
|
||||||
_entry.clear()
|
_entry.clear()
|
||||||
|
_status.text = ""
|
||||||
|
_flush_draft()
|
||||||
close_chat()
|
close_chat()
|
||||||
|
|
||||||
|
|
||||||
func _on_message(message: Dictionary) -> void:
|
func _on_message(message: Dictionary) -> void:
|
||||||
_last_message_time = Time.get_ticks_msec() / 1000.0
|
_last_message_time = Time.get_ticks_msec() / 1000.0
|
||||||
_refresh_history()
|
_refresh_history()
|
||||||
|
if _collapsed:
|
||||||
|
_collapsed_has_unread = true
|
||||||
|
_unread_indicator.show()
|
||||||
|
_update_panel_opacity(true)
|
||||||
if int(message["kind"]) != NetworkChatProtocol.Kind.PLAYER:
|
if int(message["kind"]) != NetworkChatProtocol.Kind.PLAYER:
|
||||||
return
|
return
|
||||||
var peer_id: int = message["sender_peer_id"]
|
var peer_id: int = message["sender_peer_id"]
|
||||||
_on_peer_removed(peer_id)
|
_on_peer_removed(peer_id)
|
||||||
|
var bubble := PanelContainer.new()
|
||||||
|
bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
bubble.custom_minimum_size = Vector2(270, 0)
|
||||||
|
bubble.size = Vector2(270, 72)
|
||||||
|
bubble.add_theme_stylebox_override("panel", _speech_panel_style())
|
||||||
var label := Label.new()
|
var label := Label.new()
|
||||||
label.text = "%s: %s" % [
|
label.text = str(message["body"]).left(120)
|
||||||
str(message["sender_display_name"]), str(message["body"]).left(120),
|
label.custom_minimum_size = Vector2(246, 46)
|
||||||
]
|
label.size = Vector2(246, 62)
|
||||||
label.custom_minimum_size = Vector2(80, 34)
|
|
||||||
label.size = Vector2(280, 70)
|
|
||||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
_speech_layer.add_child(label)
|
label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
|
||||||
|
label.add_theme_color_override("font_color", UtilityPageStyle.INK)
|
||||||
|
bubble.add_child(label)
|
||||||
|
_speech_layer.add_child(bubble)
|
||||||
_speech[peer_id] = {
|
_speech[peer_id] = {
|
||||||
"label": label,
|
"bubble": bubble,
|
||||||
"expires": Time.get_ticks_msec() / 1000.0 + SPEECH_SECONDS,
|
"expires": Time.get_ticks_msec() / 1000.0 + SPEECH_SECONDS,
|
||||||
"fingerprint": str(message.get("sender_fingerprint", "")),
|
"fingerprint": str(message.get("sender_fingerprint", "")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func _on_history(_messages: Array) -> void:
|
func _on_history(messages: Array) -> void:
|
||||||
for peer_id: int in _speech.keys():
|
for peer_id: int in _speech.keys():
|
||||||
var state: Dictionary = _speech[peer_id]
|
var state: Dictionary = _speech[peer_id]
|
||||||
if (
|
if (
|
||||||
_messages.is_empty()
|
messages.is_empty()
|
||||||
or _service.is_sender_filtered(
|
or _service.is_sender_filtered(
|
||||||
str(state.get("fingerprint", ""))
|
str(state.get("fingerprint", ""))
|
||||||
)
|
)
|
||||||
|
|
@ -211,7 +345,10 @@ func _on_history(_messages: Array) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_rejected(message: String) -> void:
|
func _on_rejected(message: String) -> void:
|
||||||
_entry.placeholder_text = message
|
_send_pending = false
|
||||||
|
_pending_send_body = ""
|
||||||
|
_entry.editable = true
|
||||||
|
_status.text = message
|
||||||
if not _opened:
|
if not _opened:
|
||||||
open_chat()
|
open_chat()
|
||||||
|
|
||||||
|
|
@ -235,20 +372,111 @@ func _refresh_history() -> void:
|
||||||
func _refresh_visibility() -> void:
|
func _refresh_visibility() -> void:
|
||||||
if not _available:
|
if not _available:
|
||||||
_panel.hide()
|
_panel.hide()
|
||||||
|
_collapse_button.hide()
|
||||||
|
_unread_indicator.hide()
|
||||||
|
_hint.hide()
|
||||||
return
|
return
|
||||||
if _opened:
|
_collapse_button.show()
|
||||||
_panel.show()
|
_panel.visible = not _collapsed
|
||||||
|
_unread_indicator.visible = _collapsed and _collapsed_has_unread
|
||||||
|
_hint.visible = not _opened
|
||||||
|
|
||||||
|
|
||||||
|
func _set_collapsed(value: bool, persist: bool = true) -> void:
|
||||||
|
_collapsed = value
|
||||||
|
_collapse_button.text = "›" if _collapsed else "‹"
|
||||||
|
_collapse_button.tooltip_text = (
|
||||||
|
"Expand chat" if _collapsed else "Collapse chat"
|
||||||
|
)
|
||||||
|
if not _collapsed:
|
||||||
|
_collapsed_has_unread = false
|
||||||
|
_unread_indicator.hide()
|
||||||
|
if persist and _settings != null:
|
||||||
|
_settings.update_chat_preferences(_entry.text, _collapsed)
|
||||||
|
_draft_save_timer.start()
|
||||||
|
_refresh_visibility()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_panel_opacity(immediate_recheck: bool = false) -> void:
|
||||||
|
if _panel == null or _collapsed or not _available:
|
||||||
return
|
return
|
||||||
var recent := (
|
var recent := (
|
||||||
Time.get_ticks_msec() / 1000.0 - _last_message_time
|
Time.get_ticks_msec() / 1000.0 - _last_message_time < RECENT_SECONDS
|
||||||
< COMPACT_SECONDS
|
|
||||||
)
|
)
|
||||||
_panel.visible = recent and _service != null and not _service.get_history().is_empty()
|
var focus_owner := get_viewport().gui_get_focus_owner()
|
||||||
|
var wants_full := (
|
||||||
|
_opened or recent or _panel_hovered
|
||||||
|
or focus_owner == _collapse_button
|
||||||
|
or focus_owner == _entry
|
||||||
|
)
|
||||||
|
var desired := 1.0 if wants_full else IDLE_ALPHA
|
||||||
|
if not immediate_recheck and is_equal_approx(desired, _target_alpha):
|
||||||
|
return
|
||||||
|
_target_alpha = desired
|
||||||
|
if _opacity_tween != null and _opacity_tween.is_valid():
|
||||||
|
_opacity_tween.kill()
|
||||||
|
_opacity_tween = create_tween()
|
||||||
|
_opacity_tween.tween_property(
|
||||||
|
_panel, "modulate:a", desired, 0.18
|
||||||
|
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||||
|
|
||||||
|
|
||||||
|
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 bubble == null or now >= float(state.get("expires", 0.0)):
|
||||||
|
if bubble != null:
|
||||||
|
bubble.queue_free()
|
||||||
|
_speech.erase(peer_id)
|
||||||
|
continue
|
||||||
|
var avatar := _spawn.get_avatar(peer_id)
|
||||||
|
var camera := _player.get_gameplay_camera()
|
||||||
|
if avatar == null or camera == null:
|
||||||
|
bubble.hide()
|
||||||
|
continue
|
||||||
|
var world_position := avatar.get_chat_anchor_position()
|
||||||
|
if camera.is_position_behind(world_position):
|
||||||
|
bubble.hide()
|
||||||
|
continue
|
||||||
|
var screen_position := camera.unproject_position(world_position)
|
||||||
|
var viewport_size := get_viewport_rect().size
|
||||||
|
if (
|
||||||
|
screen_position.x < -80.0
|
||||||
|
or screen_position.x > viewport_size.x + 80.0
|
||||||
|
or screen_position.y < -80.0
|
||||||
|
or screen_position.y > viewport_size.y + 80.0
|
||||||
|
):
|
||||||
|
bubble.hide()
|
||||||
|
continue
|
||||||
|
var desired := screen_position - Vector2(
|
||||||
|
bubble.size.x * 0.5, bubble.size.y + 8.0
|
||||||
|
)
|
||||||
|
bubble.position = Vector2(
|
||||||
|
clampf(desired.x, 8.0, viewport_size.x - bubble.size.x - 8.0),
|
||||||
|
clampf(desired.y, 8.0, viewport_size.y - bubble.size.y - 8.0),
|
||||||
|
)
|
||||||
|
bubble.show()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_draft_changed(_value: String) -> void:
|
||||||
|
if _settings == null:
|
||||||
|
return
|
||||||
|
_settings.update_chat_preferences(_entry.text, _collapsed)
|
||||||
|
_draft_save_timer.start()
|
||||||
|
|
||||||
|
|
||||||
|
func _flush_draft() -> void:
|
||||||
|
if _settings == null or _entry == null:
|
||||||
|
return
|
||||||
|
_settings.update_chat_preferences(_entry.text, _collapsed)
|
||||||
|
_settings.save_chat_preferences()
|
||||||
|
|
||||||
|
|
||||||
func _on_peer_removed(peer_id: int) -> void:
|
func _on_peer_removed(peer_id: int) -> void:
|
||||||
var state: Dictionary = _speech.get(peer_id, {})
|
var state: Dictionary = _speech.get(peer_id, {})
|
||||||
var label := state.get("label") as Label
|
var bubble := state.get("bubble") as PanelContainer
|
||||||
if label != null:
|
if bubble != null:
|
||||||
label.queue_free()
|
bubble.queue_free()
|
||||||
_speech.erase(peer_id)
|
_speech.erase(peer_id)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ const PlayerCoolerCapacityType = preload(
|
||||||
"res://progression/player_cooler_capacity.gd"
|
"res://progression/player_cooler_capacity.gd"
|
||||||
)
|
)
|
||||||
const ChatUIType = preload("res://ui/chat_ui.gd")
|
const ChatUIType = preload("res://ui/chat_ui.gd")
|
||||||
|
const PlayerSettingsManagerType = preload(
|
||||||
|
"res://settings/player_settings_manager.gd"
|
||||||
|
)
|
||||||
|
|
||||||
signal pixelation_settings_visibility_changed(is_visible: bool)
|
signal pixelation_settings_visibility_changed(is_visible: bool)
|
||||||
signal crisp_reset_focus_requested
|
signal crisp_reset_focus_requested
|
||||||
|
|
@ -114,12 +117,13 @@ func setup(
|
||||||
reservations: PlayerAssetReservationService,
|
reservations: PlayerAssetReservationService,
|
||||||
network_profile_service: NetworkProfileService,
|
network_profile_service: NetworkProfileService,
|
||||||
network_player_list: NetworkPlayerListService,
|
network_player_list: NetworkPlayerListService,
|
||||||
|
settings_manager: PlayerSettingsManagerType,
|
||||||
) -> void:
|
) -> void:
|
||||||
_fishing_spot = fishing_spot
|
_fishing_spot = fishing_spot
|
||||||
_item_effects = item_effects
|
_item_effects = item_effects
|
||||||
_chat_ui.setup(
|
_chat_ui.setup(
|
||||||
network_chat_service, network_session, spawn_service, player,
|
network_chat_service, network_session, spawn_service, player,
|
||||||
fishing_spot
|
fishing_spot, settings_manager
|
||||||
)
|
)
|
||||||
_title_settings_panel.setup_network_profile(
|
_title_settings_panel.setup_network_profile(
|
||||||
network_profile, network_session
|
network_profile, network_session
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue