forked from xiangwang25/whale-town-front-v2
feat: expand multiplayer, chat, and release support
- add network NPC synchronization and dialogue interactions - add world bulletin publishing and display - improve authentication, session refresh, and appearance sync - synchronize player direction and movement animations - improve input focus and progressive Web loading - add macOS/Windows builds and deployment configuration - include required fonts, shaders, and runtime assets
This commit is contained in:
@@ -56,6 +56,9 @@ extends Control
|
||||
@onready var friends_list_surface: Control = %FriendsListSurface
|
||||
@onready var add_friend_row: HBoxContainer = %AddFriendRow
|
||||
@onready var add_friend_button: Button = %AddFriendButton
|
||||
@onready var tabs: HBoxContainer = $ChatPanel/PanelMargin/ContentVBox/Tabs
|
||||
@onready var panel_margin: MarginContainer = $ChatPanel/PanelMargin
|
||||
@onready var input_row: HBoxContainer = $ChatPanel/PanelMargin/ContentVBox/InputRow
|
||||
|
||||
# ============================================================================
|
||||
# 预加载资源
|
||||
@@ -80,6 +83,9 @@ const TAB_WHISPER: String = "whisper"
|
||||
const TAB_FRIENDS: String = "friends"
|
||||
const MAX_DISPLAYED_MESSAGES: int = 100
|
||||
const MOVEMENT_ACTIONS: Array[String] = ["move_left", "move_right", "move_up", "move_down"]
|
||||
const SEND_ICON := preload("res://assets/ui/world_bulletin/world_bulletin_send_v2_128.png")
|
||||
const STATIC_NPC_DIALOGUE_PREFIX: String = "static_npc_dialogue:"
|
||||
const DEFAULT_WHISPER_TAB_TEXT: String = "悄悄话"
|
||||
|
||||
# ============================================================================
|
||||
# 成员变量
|
||||
@@ -106,6 +112,9 @@ var _current_tab: String = TAB_WORLD
|
||||
# 悄悄话目标(靠近玩家按 E 后设置)
|
||||
var _whisper_target_user_id: String = ""
|
||||
var _whisper_target_username: String = ""
|
||||
var _npc_target_id: String = ""
|
||||
var _npc_target_name: String = ""
|
||||
var _npc_session_id: String = ""
|
||||
|
||||
# 好友私聊目标(好友列表接入后复用同一私聊协议)
|
||||
var _friend_target_user_id: String = ""
|
||||
@@ -118,6 +127,12 @@ var _friends_status_message: String = ""
|
||||
var _messages: Array[Dictionary] = []
|
||||
|
||||
var _send_failure_handled_by_ui: bool = false
|
||||
var _npc_thinking_row: Control
|
||||
var _npc_thinking_timer: Timer
|
||||
var _npc_dialogue_mode: bool = false
|
||||
var _npc_dialogue_read_only: bool = false
|
||||
var _default_chat_panel_style: StyleBox
|
||||
var _default_input_shell_style: StyleBox
|
||||
|
||||
# ============================================================================
|
||||
# 生命周期方法
|
||||
@@ -126,12 +141,17 @@ var _send_failure_handled_by_ui: bool = false
|
||||
# 准备就绪
|
||||
func _ready() -> void:
|
||||
_configure_mouse_focus()
|
||||
_configure_send_icon()
|
||||
_capture_default_dialogue_styles()
|
||||
if not get_viewport().size_changed.is_connected(_on_viewport_size_changed):
|
||||
get_viewport().size_changed.connect(_on_viewport_size_changed)
|
||||
|
||||
# 初始隐藏聊天框
|
||||
hide_chat(true)
|
||||
|
||||
# 创建隐藏计时器
|
||||
_create_hide_timer()
|
||||
_create_npc_thinking_timer()
|
||||
|
||||
# 订阅事件(Call Down via EventSystem)
|
||||
_subscribe_to_events()
|
||||
@@ -144,6 +164,134 @@ func _ready() -> void:
|
||||
_update_tab_visuals()
|
||||
_update_bubble_send_button_visibility()
|
||||
|
||||
func _configure_send_icon() -> void:
|
||||
if not is_instance_valid(send_button):
|
||||
return
|
||||
send_button.text = ""
|
||||
send_button.icon = SEND_ICON
|
||||
send_button.expand_icon = true
|
||||
send_button.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
|
||||
func _capture_default_dialogue_styles() -> void:
|
||||
if is_instance_valid(chat_panel):
|
||||
_default_chat_panel_style = chat_panel.get_theme_stylebox("panel").duplicate() as StyleBox
|
||||
if is_instance_valid(input_shell):
|
||||
_default_input_shell_style = input_shell.get_theme_stylebox("panel").duplicate() as StyleBox
|
||||
|
||||
func _set_npc_dialogue_mode(enabled: bool, readOnly: bool = false) -> void:
|
||||
_npc_dialogue_mode = enabled
|
||||
_npc_dialogue_read_only = enabled and readOnly
|
||||
|
||||
var worldTab := popular_tab_button.get_parent() as Control if is_instance_valid(popular_tab_button) else null
|
||||
var whisperTab := recent_tab_button.get_parent() as Control if is_instance_valid(recent_tab_button) else null
|
||||
var friendsTab := friends_tab_button.get_parent() as Control if is_instance_valid(friends_tab_button) else null
|
||||
if is_instance_valid(worldTab):
|
||||
worldTab.visible = not enabled
|
||||
if is_instance_valid(friendsTab):
|
||||
friendsTab.visible = not enabled
|
||||
if is_instance_valid(whisperTab):
|
||||
whisperTab.visible = true
|
||||
if is_instance_valid(recent_tab_button):
|
||||
recent_tab_button.text = _npc_target_name if enabled else DEFAULT_WHISPER_TAB_TEXT
|
||||
recent_tab_button.alignment = HORIZONTAL_ALIGNMENT_LEFT if enabled else HORIZONTAL_ALIGNMENT_CENTER
|
||||
recent_tab_button.mouse_filter = Control.MOUSE_FILTER_IGNORE if enabled else Control.MOUSE_FILTER_STOP
|
||||
if is_instance_valid(input_row):
|
||||
input_row.visible = not _npc_dialogue_read_only
|
||||
|
||||
if is_instance_valid(chat_panel):
|
||||
var panelStyle := _create_npc_dialogue_style() if enabled else _default_chat_panel_style
|
||||
if panelStyle != null:
|
||||
chat_panel.add_theme_stylebox_override("panel", panelStyle)
|
||||
if is_instance_valid(input_shell):
|
||||
var inputStyle := _create_npc_dialogue_input_style() if enabled else _default_input_shell_style
|
||||
if inputStyle != null:
|
||||
input_shell.add_theme_stylebox_override("panel", inputStyle)
|
||||
if is_instance_valid(panel_margin):
|
||||
var horizontalMargin := 24 if enabled else 20
|
||||
var verticalMargin := 18
|
||||
panel_margin.add_theme_constant_override("margin_left", horizontalMargin)
|
||||
panel_margin.add_theme_constant_override("margin_top", verticalMargin)
|
||||
panel_margin.add_theme_constant_override("margin_right", horizontalMargin)
|
||||
panel_margin.add_theme_constant_override("margin_bottom", verticalMargin)
|
||||
|
||||
_apply_chat_panel_layout()
|
||||
_update_tab_visuals()
|
||||
if is_instance_valid(message_list):
|
||||
_rerender_messages()
|
||||
|
||||
func _clear_npc_dialogue_target() -> void:
|
||||
_npc_target_id = ""
|
||||
_npc_target_name = ""
|
||||
_npc_session_id = ""
|
||||
_set_npc_dialogue_mode(false, false)
|
||||
|
||||
func _apply_chat_panel_layout() -> void:
|
||||
if not is_instance_valid(chat_panel):
|
||||
return
|
||||
if not _npc_dialogue_mode:
|
||||
chat_panel.anchor_left = 0.024
|
||||
chat_panel.anchor_top = 1.0
|
||||
chat_panel.anchor_right = 0.024
|
||||
chat_panel.anchor_bottom = 1.0
|
||||
chat_panel.offset_left = 0.0
|
||||
chat_panel.offset_top = -448.0
|
||||
chat_panel.offset_right = 548.0
|
||||
chat_panel.offset_bottom = -32.0
|
||||
return
|
||||
|
||||
var viewportSize := get_viewport_rect().size
|
||||
var dialogWidth := clampf(viewportSize.x * 0.90, 320.0, 760.0)
|
||||
var minimumHeight := 200.0 if _npc_dialogue_read_only else 250.0
|
||||
var maximumHeight := 250.0 if _npc_dialogue_read_only else 340.0
|
||||
var dialogHeight := clampf(viewportSize.y * 0.42, minimumHeight, maximumHeight)
|
||||
var bottomMargin := clampf(viewportSize.y * 0.04, 18.0, 34.0)
|
||||
chat_panel.anchor_left = 0.5
|
||||
chat_panel.anchor_top = 1.0
|
||||
chat_panel.anchor_right = 0.5
|
||||
chat_panel.anchor_bottom = 1.0
|
||||
chat_panel.offset_left = -dialogWidth * 0.5
|
||||
chat_panel.offset_top = -bottomMargin - dialogHeight
|
||||
chat_panel.offset_right = dialogWidth * 0.5
|
||||
chat_panel.offset_bottom = -bottomMargin
|
||||
|
||||
func _on_viewport_size_changed() -> void:
|
||||
_apply_chat_panel_layout()
|
||||
|
||||
func _create_npc_dialogue_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(1.0, 0.985, 0.91, 0.985)
|
||||
style.border_color = Color(0.055, 0.22, 0.235, 1.0)
|
||||
style.border_width_left = 4
|
||||
style.border_width_top = 4
|
||||
style.border_width_right = 4
|
||||
style.border_width_bottom = 4
|
||||
style.corner_radius_top_left = 5
|
||||
style.corner_radius_top_right = 5
|
||||
style.corner_radius_bottom_left = 5
|
||||
style.corner_radius_bottom_right = 5
|
||||
style.shadow_color = Color(0.02, 0.06, 0.07, 0.3)
|
||||
style.shadow_size = 8
|
||||
style.shadow_offset = Vector2(0, 5)
|
||||
return style
|
||||
|
||||
func _create_npc_dialogue_input_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(1.0, 1.0, 0.98, 1.0)
|
||||
style.border_color = Color(0.22, 0.42, 0.42, 0.82)
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
style.corner_radius_top_left = 4
|
||||
style.corner_radius_top_right = 4
|
||||
style.corner_radius_bottom_left = 4
|
||||
style.corner_radius_bottom_right = 4
|
||||
style.content_margin_left = 14
|
||||
style.content_margin_top = 6
|
||||
style.content_margin_right = 14
|
||||
style.content_margin_bottom = 6
|
||||
return style
|
||||
|
||||
# 清理
|
||||
func _exit_tree() -> void:
|
||||
# 取消事件订阅
|
||||
@@ -155,6 +303,8 @@ func _exit_tree() -> void:
|
||||
eventSystem.call("disconnect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_login_success, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CHAT_LOGIN_FAILED, _on_login_failed, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CHAT_PRIVATE_TARGET_SELECTED, _on_private_target_selected, self)
|
||||
eventSystem.call("disconnect_event", EventNames.NPC_SPOKE, _on_npc_spoke, self)
|
||||
eventSystem.call("disconnect_event", EventNames.NPC_INTERACTION_ERROR, _on_npc_interaction_error, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CHAT_FRIEND_SELECTED, _on_friend_selected, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CHAT_FRIENDS_UPDATED, _on_friends_updated, self)
|
||||
eventSystem.call("disconnect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self)
|
||||
@@ -162,6 +312,11 @@ func _exit_tree() -> void:
|
||||
# 清理计时器
|
||||
if _hide_timer:
|
||||
_hide_timer.queue_free()
|
||||
if _npc_thinking_timer:
|
||||
_npc_thinking_timer.queue_free()
|
||||
_hide_npc_thinking()
|
||||
if get_viewport() != null and get_viewport().size_changed.is_connected(_on_viewport_size_changed):
|
||||
get_viewport().size_changed.disconnect(_on_viewport_size_changed)
|
||||
|
||||
if is_instance_valid(_transition_tween):
|
||||
_transition_tween.kill()
|
||||
@@ -192,6 +347,10 @@ func _input(event: InputEvent) -> void:
|
||||
var key_event := event as InputEventKey
|
||||
if not key_event.pressed or key_event.echo:
|
||||
return
|
||||
if key_event.keycode == KEY_ESCAPE and _is_chat_visible:
|
||||
hide_chat()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
||||
# T 键用于唤起聊天(输入框聚焦时不拦截)
|
||||
if key_event.keycode == KEY_T and not chat_input.has_focus():
|
||||
@@ -264,6 +423,9 @@ func _update_input_placeholder() -> void:
|
||||
if _current_tab == TAB_WHISPER and not _whisper_target_username.is_empty():
|
||||
chat_input.placeholder_text = "对 %s 说点什么..." % _whisper_target_username
|
||||
return
|
||||
if _current_tab == TAB_WHISPER and not _npc_target_name.is_empty():
|
||||
chat_input.placeholder_text = "对 %s 说点什么..." % _npc_target_name
|
||||
return
|
||||
|
||||
if _current_tab == TAB_FRIENDS and not _friend_target_username.is_empty():
|
||||
chat_input.placeholder_text = "对 %s 说点什么..." % _friend_target_username
|
||||
@@ -338,6 +500,8 @@ func show_chat(immediate: bool = false) -> void:
|
||||
|
||||
# 隐藏聊天框
|
||||
func hide_chat(immediate: bool = false) -> void:
|
||||
_end_npc_session()
|
||||
_clear_npc_dialogue_target()
|
||||
_is_chat_visible = false
|
||||
_is_typing = false
|
||||
|
||||
@@ -374,6 +538,13 @@ func _create_hide_timer() -> void:
|
||||
_hide_timer.timeout.connect(_on_hide_timeout)
|
||||
add_child(_hide_timer)
|
||||
|
||||
func _create_npc_thinking_timer() -> void:
|
||||
_npc_thinking_timer = Timer.new()
|
||||
_npc_thinking_timer.wait_time = 60.0
|
||||
_npc_thinking_timer.one_shot = true
|
||||
_npc_thinking_timer.timeout.connect(_on_npc_thinking_timeout)
|
||||
add_child(_npc_thinking_timer)
|
||||
|
||||
# 开始隐藏倒计时
|
||||
func _start_hide_timer() -> void:
|
||||
if _is_typing:
|
||||
@@ -508,6 +679,17 @@ func _send_input_message(show_bubble: bool) -> void:
|
||||
|
||||
# 清空输入框
|
||||
chat_input.clear()
|
||||
if not _npc_target_id.is_empty() and _current_tab == TAB_WHISPER:
|
||||
_add_message_data({
|
||||
"from_user": _current_username,
|
||||
"content": content,
|
||||
"timestamp": Time.get_unix_time_from_system(),
|
||||
"is_self": true,
|
||||
"scope": "private",
|
||||
"private_context": TAB_WHISPER,
|
||||
"npc_id": _npc_target_id,
|
||||
})
|
||||
_show_npc_thinking()
|
||||
|
||||
# 发送后延迟重新聚焦,避免被 LineEdit 的提交事件在同一帧内抢走焦点
|
||||
call_deferred("_focus_input_after_send")
|
||||
@@ -565,6 +747,8 @@ func _subscribe_to_events() -> void:
|
||||
|
||||
# 订阅近身悄悄话目标选择事件
|
||||
eventSystem.call("connect_event", EventNames.CHAT_PRIVATE_TARGET_SELECTED, _on_private_target_selected, self)
|
||||
eventSystem.call("connect_event", EventNames.NPC_SPOKE, _on_npc_spoke, self)
|
||||
eventSystem.call("connect_event", EventNames.NPC_INTERACTION_ERROR, _on_npc_interaction_error, self)
|
||||
|
||||
# 订阅右下角好友列表的好友选择事件
|
||||
eventSystem.call("connect_event", EventNames.CHAT_FRIEND_SELECTED, _on_friend_selected, self)
|
||||
@@ -611,9 +795,83 @@ func _on_chat_error(data: Dictionary) -> void:
|
||||
if _current_tab == TAB_FRIENDS:
|
||||
_render_friend_conversation_header()
|
||||
return
|
||||
if not _npc_target_id.is_empty() and _current_tab == TAB_WHISPER:
|
||||
_hide_npc_thinking()
|
||||
if not message.strip_edges().is_empty():
|
||||
_add_system_message(message)
|
||||
|
||||
func _on_npc_spoke(data: Dictionary) -> void:
|
||||
var npc_id := str(data.get("npc_id", data.get("npcId", ""))).strip_edges()
|
||||
if _npc_target_id.is_empty() or npc_id != _npc_target_id:
|
||||
return
|
||||
var response := str(data.get("response", "")).strip_edges()
|
||||
if response.is_empty():
|
||||
return
|
||||
_hide_npc_thinking()
|
||||
var session_id := str(data.get("session_id", data.get("sessionId", ""))).strip_edges()
|
||||
if not session_id.is_empty():
|
||||
_npc_session_id = session_id
|
||||
_add_message_data({
|
||||
"from_user": str(data.get("npc_name", data.get("npcName", _npc_target_name))),
|
||||
"content": response,
|
||||
"timestamp": Time.get_unix_time_from_system(),
|
||||
"is_self": false,
|
||||
"scope": "private",
|
||||
"private_context": TAB_WHISPER,
|
||||
"npc_id": npc_id,
|
||||
})
|
||||
|
||||
func _on_npc_interaction_error(data: Dictionary) -> void:
|
||||
var npc_id := str(data.get("npc_id", data.get("npcId", ""))).strip_edges()
|
||||
if not _npc_target_id.is_empty() and not npc_id.is_empty() and npc_id != _npc_target_id:
|
||||
return
|
||||
_hide_npc_thinking()
|
||||
var message := str(data.get("message", "NPC暂时无法回应")).strip_edges()
|
||||
if not message.is_empty():
|
||||
_add_system_message(message)
|
||||
|
||||
func _show_npc_thinking() -> void:
|
||||
_hide_npc_thinking()
|
||||
if _npc_target_id.is_empty() or _current_tab != TAB_WHISPER:
|
||||
return
|
||||
if not is_instance_valid(message_list):
|
||||
return
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.name = "NpcThinkingRow"
|
||||
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
message_list.add_child(row)
|
||||
|
||||
var message_node := chat_message_scene.instantiate() as Control
|
||||
if message_node == null:
|
||||
row.queue_free()
|
||||
return
|
||||
row.add_child(message_node)
|
||||
if message_node.has_method("set_message"):
|
||||
message_node.call("set_message", _npc_target_name, "正在思考...", Time.get_unix_time_from_system(), false)
|
||||
if message_node.has_method("set_dialogue_mode"):
|
||||
message_node.call("set_dialogue_mode", _npc_dialogue_mode)
|
||||
message_node.modulate = Color(1.0, 1.0, 1.0, 0.78)
|
||||
_npc_thinking_row = row
|
||||
if is_instance_valid(_npc_thinking_timer):
|
||||
_npc_thinking_timer.start()
|
||||
call_deferred("_scroll_to_bottom")
|
||||
|
||||
func _hide_npc_thinking() -> void:
|
||||
if is_instance_valid(_npc_thinking_timer):
|
||||
_npc_thinking_timer.stop()
|
||||
if is_instance_valid(_npc_thinking_row):
|
||||
_npc_thinking_row.queue_free()
|
||||
_npc_thinking_row = null
|
||||
|
||||
func _on_npc_thinking_timeout() -> void:
|
||||
if not is_instance_valid(_npc_thinking_row):
|
||||
return
|
||||
_hide_npc_thinking()
|
||||
_add_system_message("NPC暂时没有回应,请稍后再试")
|
||||
|
||||
# 处理连接状态变化
|
||||
func _on_connection_state_changed(_data: Dictionary) -> void:
|
||||
# 连接状态变化处理(当前不更新UI)
|
||||
@@ -699,6 +957,8 @@ func _send_current_tab_message(chatManager: Node, content: String, show_bubble:
|
||||
TAB_WORLD:
|
||||
return bool(chatManager.call("send_chat_message", content, "global", show_bubble))
|
||||
TAB_WHISPER:
|
||||
if not _npc_target_id.is_empty():
|
||||
return bool(chatManager.call("interact_with_world_npc", _npc_target_id, content, _npc_session_id))
|
||||
if _whisper_target_user_id.is_empty():
|
||||
_add_system_message("请靠近玩家按 E 发起悄悄话")
|
||||
_send_failure_handled_by_ui = true
|
||||
@@ -734,6 +994,8 @@ func start_whisper(user_id: String, username: String = "") -> void:
|
||||
if normalized_user_id.is_empty():
|
||||
return
|
||||
|
||||
_end_npc_session()
|
||||
_clear_npc_dialogue_target()
|
||||
_whisper_target_user_id = normalized_user_id
|
||||
_whisper_target_username = username.strip_edges()
|
||||
if _whisper_target_username.is_empty():
|
||||
@@ -744,6 +1006,66 @@ func start_whisper(user_id: String, username: String = "") -> void:
|
||||
show_chat(true)
|
||||
call_deferred("_focus_input_after_send")
|
||||
|
||||
func start_npc_whisper(npc_id: String, npc_name: String = "NPC", greeting: String = "") -> void:
|
||||
var normalized_id := npc_id.strip_edges()
|
||||
if normalized_id.is_empty():
|
||||
return
|
||||
_end_npc_session()
|
||||
_clear_npc_dialogue_target()
|
||||
_hide_npc_thinking()
|
||||
_npc_target_id = normalized_id
|
||||
_npc_target_name = npc_name.strip_edges() if not npc_name.strip_edges().is_empty() else "NPC"
|
||||
_npc_session_id = ""
|
||||
_whisper_target_user_id = ""
|
||||
_whisper_target_username = ""
|
||||
select_tab(TAB_WHISPER)
|
||||
_set_npc_dialogue_mode(true, false)
|
||||
show_chat(true)
|
||||
if not greeting.strip_edges().is_empty():
|
||||
_add_message_data({
|
||||
"from_user": _npc_target_name,
|
||||
"content": greeting.strip_edges(),
|
||||
"timestamp": Time.get_unix_time_from_system(),
|
||||
"is_self": false,
|
||||
"scope": "private",
|
||||
"private_context": TAB_WHISPER,
|
||||
"npc_id": _npc_target_id,
|
||||
})
|
||||
call_deferred("_focus_input_after_send")
|
||||
|
||||
func show_npc_dialogue(npc_name: String, text: String) -> void:
|
||||
var normalizedText := text.strip_edges()
|
||||
if normalizedText.is_empty():
|
||||
return
|
||||
_end_npc_session()
|
||||
_clear_npc_dialogue_target()
|
||||
_hide_npc_thinking()
|
||||
_npc_target_name = npc_name.strip_edges() if not npc_name.strip_edges().is_empty() else "NPC"
|
||||
_npc_target_id = "%s%s:%d" % [STATIC_NPC_DIALOGUE_PREFIX, _npc_target_name, Time.get_ticks_msec()]
|
||||
_npc_session_id = ""
|
||||
_whisper_target_user_id = ""
|
||||
_whisper_target_username = ""
|
||||
select_tab(TAB_WHISPER)
|
||||
_set_npc_dialogue_mode(true, true)
|
||||
show_chat(true)
|
||||
_add_message_data({
|
||||
"from_user": _npc_target_name,
|
||||
"content": normalizedText,
|
||||
"timestamp": Time.get_unix_time_from_system(),
|
||||
"is_self": false,
|
||||
"scope": "private",
|
||||
"private_context": TAB_WHISPER,
|
||||
"npc_id": _npc_target_id,
|
||||
})
|
||||
|
||||
func _end_npc_session() -> void:
|
||||
if _npc_target_id.is_empty() or _npc_session_id.is_empty() or _npc_target_id.begins_with(STATIC_NPC_DIALOGUE_PREFIX):
|
||||
return
|
||||
var chat_manager := _get_chat_manager()
|
||||
if chat_manager != null and chat_manager.has_method("end_world_npc_session"):
|
||||
chat_manager.call("end_world_npc_session", _npc_target_id, _npc_session_id)
|
||||
_npc_session_id = ""
|
||||
|
||||
func add_whisper_target_as_friend() -> bool:
|
||||
if _whisper_target_user_id.is_empty():
|
||||
_add_system_message("请先靠近玩家按 F")
|
||||
@@ -780,6 +1102,8 @@ func select_friend_private_target(user_id: String, username: String = "") -> voi
|
||||
if normalized_user_id.is_empty():
|
||||
return
|
||||
|
||||
_end_npc_session()
|
||||
_clear_npc_dialogue_target()
|
||||
_friend_target_user_id = normalized_user_id
|
||||
_friend_target_username = username.strip_edges()
|
||||
if _friend_target_username.is_empty():
|
||||
@@ -934,15 +1258,23 @@ func _add_message_data(message: Dictionary) -> void:
|
||||
_render_message(message)
|
||||
|
||||
func _render_message(message: Dictionary) -> void:
|
||||
# 如果聊天框隐藏,自动显示
|
||||
# 世界频道消息进入公告 HUD,不自动抢占地图视野;
|
||||
# 私聊、NPC 回复和用户主动发送的消息仍可唤起聊天面板。
|
||||
if not _is_chat_visible:
|
||||
var message_scope := str(message.get("scope", "global")).strip_edges().to_lower()
|
||||
var is_private := bool(message.get("is_private", false)) or message_scope == "private"
|
||||
var is_npc := not str(message.get("npc_id", "")).strip_edges().is_empty()
|
||||
# 系统存在/欢迎消息也属于世界公告,不应自动打开旧聊天窗口。
|
||||
# 只有私聊和 NPC 会话需要在收到回复时主动唤起聊天 UI。
|
||||
if not is_private and not is_npc:
|
||||
return
|
||||
show_chat()
|
||||
|
||||
# 每条消息用一行容器包起来,方便左右对齐且不挤在一起
|
||||
var row := HBoxContainer.new()
|
||||
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
row.alignment = BoxContainer.ALIGNMENT_END if bool(message.get("is_self", false)) else BoxContainer.ALIGNMENT_BEGIN
|
||||
row.alignment = BoxContainer.ALIGNMENT_BEGIN if _npc_dialogue_mode else (BoxContainer.ALIGNMENT_END if bool(message.get("is_self", false)) else BoxContainer.ALIGNMENT_BEGIN)
|
||||
|
||||
# 创建消息节点
|
||||
var message_node: Control = chat_message_scene.instantiate() as Control
|
||||
@@ -962,6 +1294,8 @@ func _render_message(message: Dictionary) -> void:
|
||||
float(message.get("timestamp", 0.0)),
|
||||
bool(message.get("is_self", false))
|
||||
)
|
||||
if message_node.has_method("set_dialogue_mode"):
|
||||
message_node.call("set_dialogue_mode", _npc_dialogue_mode)
|
||||
|
||||
# 自动滚动到底部
|
||||
call_deferred("_scroll_to_bottom")
|
||||
@@ -993,6 +1327,9 @@ func _message_matches_current_tab(message: Dictionary) -> bool:
|
||||
return true
|
||||
|
||||
func _private_message_matches_current_tab(message: Dictionary) -> bool:
|
||||
var npc_id := str(message.get("npc_id", message.get("npcId", ""))).strip_edges()
|
||||
if _current_tab == TAB_WHISPER and not _npc_target_id.is_empty():
|
||||
return npc_id == _npc_target_id
|
||||
var scope := str(message.get("scope", "local")).strip_edges().to_lower()
|
||||
var is_private := bool(message.get("is_private", false)) or scope == "private"
|
||||
if not is_private:
|
||||
|
||||
Reference in New Issue
Block a user