441 lines
11 KiB
C#
441 lines
11 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Logic.Audio;
|
||
using Logic.Multilingual;
|
||
using TH1_Logic.Chat;
|
||
using TH1_Logic.Net;
|
||
using TH1_Logic.Steam;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
namespace TH1_UI.View.Common
|
||
{
|
||
/// <summary>
|
||
/// 通用聊天区域,由需要聊天功能的 UI 动态挂载。
|
||
/// 拉取模式:订阅 ChatManager.OnChatUpdated,对比 _displayedCount 增量显示新消息。
|
||
/// </summary>
|
||
public class UIChatAreaMono : MonoBehaviour
|
||
{
|
||
// ---- 对外事件 ----
|
||
public Action<string> OnSendMessage;
|
||
|
||
// ---- 静态访问,供 InputLogic 查询/操作输入框 ----
|
||
public static bool IsInputFieldFocused => _instance != null
|
||
&& _instance._inputAreaVisible
|
||
&& _instance.TextInputArea != null
|
||
&& _instance.TextInputArea.isFocused;
|
||
|
||
public static bool IsInputAreaVisible => _instance != null
|
||
&& _instance._inputAreaVisible;
|
||
|
||
public static bool IsActive => _instance != null && _instance._initialized;
|
||
|
||
public static void HideInputArea()
|
||
{
|
||
if (_instance != null)
|
||
_instance.SetInputAreaVisible(false);
|
||
}
|
||
|
||
public static void ShowInputArea()
|
||
{
|
||
if (_instance != null)
|
||
_instance.SetInputAreaVisible(true);
|
||
}
|
||
|
||
public static void FocusInputField()
|
||
{
|
||
if (_instance != null && _instance.TextInputArea != null)
|
||
_instance.TextInputArea.ActivateInputField();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Enter键专用:有文字就发送,没文字就关闭。
|
||
/// </summary>
|
||
public static void SendOrClose()
|
||
{
|
||
if (_instance == null) return;
|
||
if (_instance.TextInputArea == null) return;
|
||
|
||
string text = _instance.TextInputArea.text;
|
||
if (!string.IsNullOrWhiteSpace(text))
|
||
{
|
||
_instance.OnSendMessage?.Invoke(text.Trim());
|
||
_instance.TextInputArea.text = string.Empty;
|
||
_instance.TextInputArea.ActivateInputField();
|
||
}
|
||
else
|
||
{
|
||
_instance.SetInputAreaVisible(false);
|
||
}
|
||
}
|
||
|
||
private static UIChatAreaMono _instance;
|
||
|
||
// ---- Inspector 绑定 ----
|
||
[Header("ChatButton - 控制输入区显隐")]
|
||
public Button ChatButton;
|
||
|
||
[Header("ChatShowArea - 常驻消息显示区")]
|
||
public RectTransform ChatShowArea;
|
||
public GameObject ChatPrefab; // NetChat_ChatPrefab
|
||
|
||
[Header("ChatInputArea - 输入区")]
|
||
public GameObject ChatInputArea;
|
||
public TMP_InputField TextInputArea;
|
||
public Button SendButton;
|
||
|
||
[Header("DocArea - 历史聊天追溯")]
|
||
public Button DocButton;
|
||
public GameObject DocArea;
|
||
public TMP_Text DocText;
|
||
|
||
[Header("配置")]
|
||
public int MaxChatCount = 50;
|
||
public float MessageLifetime = 10f;
|
||
public int DocRecentCount = 50;
|
||
|
||
// ---- 内部数据 ----
|
||
private readonly List<ChatRowEntry> _chatRows = new List<ChatRowEntry>();
|
||
private bool _inputAreaVisible;
|
||
private bool _initialized;
|
||
|
||
private struct ChatRowEntry
|
||
{
|
||
public GameObject Go;
|
||
public float SpawnTime;
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Lifecycle
|
||
// ------------------------------------------------------------------
|
||
|
||
private void OnDestroy()
|
||
{
|
||
Shutdown();
|
||
if (_instance == this) _instance = null;
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
if (!_initialized || _chatRows.Count == 0) return;
|
||
|
||
// 从头部检查过期消息
|
||
float now = Time.time;
|
||
while (_chatRows.Count > 0)
|
||
{
|
||
var entry = _chatRows[0];
|
||
if (now - entry.SpawnTime < MessageLifetime) break;
|
||
_chatRows.RemoveAt(0);
|
||
if (entry.Go != null)
|
||
Destroy(entry.Go);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 外部调用的初始化,跟随 NetInfo 一起触发。
|
||
/// ChatButton + ChatShowArea 显示并保持活跃,InputArea 隐藏。
|
||
/// </summary>
|
||
public void Init()
|
||
{
|
||
if (_initialized) return;
|
||
_initialized = true;
|
||
_instance = this;
|
||
Debug.Log($"[ChatArea] Init: ChatPrefab={ChatPrefab != null}, ChatShowArea={ChatShowArea != null}, ChatButton={ChatButton != null}");
|
||
|
||
// 标记现有消息为已显示,不回显历史
|
||
foreach (var item in ChatManager.Instance.ChatItems)
|
||
item.AlreadyUIShow = true;
|
||
|
||
// 确保 ChatButton 和 ChatShowArea 可见
|
||
if (ChatButton != null)
|
||
ChatButton.gameObject.SetActive(true);
|
||
if (ChatShowArea != null)
|
||
ChatShowArea.gameObject.SetActive(true);
|
||
|
||
// 隐藏输入区
|
||
SetInputAreaVisible(false);
|
||
|
||
// 默认隐藏历史追溯区
|
||
SetDocAreaVisible(false);
|
||
|
||
// 设置输入框placeholder多语言文本
|
||
// 必须通过 SetUIText 设置 MultilingualTextMono 的 ID,
|
||
// 否则 OnEnable 时 MultilingualTextMono 会用旧 ID 覆盖 text
|
||
if (TextInputArea != null && TextInputArea.placeholder is TextMeshProUGUI placeholderTmp)
|
||
MultilingualManager.Instance.SetUIText(placeholderTmp, Table.Instance.TextDataAssets.NetChatPlaceHolder);
|
||
|
||
// 绑定按钮事件
|
||
if (ChatButton != null)
|
||
ChatButton.onClick.AddListener(ToggleInputArea);
|
||
if (SendButton != null)
|
||
SendButton.onClick.AddListener(OnSendButtonClick);
|
||
if (TextInputArea != null)
|
||
TextInputArea.onSubmit.AddListener(OnInputSubmit);
|
||
if (DocButton != null)
|
||
DocButton.onClick.AddListener(ToggleDocArea);
|
||
|
||
// 订阅 ChatManager 变更回调
|
||
ChatManager.Instance.OnChatUpdated += RefreshChatContent;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭并清理,可由外部或 OnDestroy 调用。
|
||
/// </summary>
|
||
public void Shutdown()
|
||
{
|
||
if (!_initialized) return;
|
||
_initialized = false;
|
||
|
||
// 取消订阅
|
||
ChatManager.Instance.OnChatUpdated -= RefreshChatContent;
|
||
|
||
if (ChatButton != null)
|
||
ChatButton.onClick.RemoveListener(ToggleInputArea);
|
||
if (SendButton != null)
|
||
SendButton.onClick.RemoveListener(OnSendButtonClick);
|
||
if (TextInputArea != null)
|
||
TextInputArea.onSubmit.RemoveListener(OnInputSubmit);
|
||
if (DocButton != null)
|
||
DocButton.onClick.RemoveListener(ToggleDocArea);
|
||
|
||
ClearAll();
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Public API
|
||
// ------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// 添加一条系统消息(不经过ChatManager,直接显示)
|
||
/// </summary>
|
||
public void AddSystemMessage(string message)
|
||
{
|
||
var data = new ChatMessageData
|
||
{
|
||
SenderName = string.Empty,
|
||
Message = message,
|
||
IsSelf = false,
|
||
IsSystem = true,
|
||
Timestamp = DateTime.Now
|
||
};
|
||
CreateChatRow(data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清空所有已显示的消息行
|
||
/// </summary>
|
||
public void ClearAll()
|
||
{
|
||
for (int i = _chatRows.Count - 1; i >= 0; i--)
|
||
{
|
||
if (_chatRows[i].Go != null)
|
||
Destroy(_chatRows[i].Go);
|
||
}
|
||
_chatRows.Clear();
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Core: 拉取 ChatManager 增量刷新
|
||
// ------------------------------------------------------------------
|
||
|
||
private void RefreshChatBox()
|
||
{
|
||
if (!_initialized)
|
||
{
|
||
Debug.Log("[ChatArea] RefreshChatBox skipped: not initialized");
|
||
return;
|
||
}
|
||
|
||
var items = ChatManager.Instance.ChatItems;
|
||
var lobby = LobbyManager.Instance.Lobby;
|
||
if (lobby == null)
|
||
{
|
||
Debug.LogWarning("[ChatArea] RefreshChatBox skipped: lobby is null");
|
||
return;
|
||
}
|
||
ulong selfId = lobby.GetSelfMemberId();
|
||
|
||
int newCount = 0;
|
||
for (int i = 0; i < items.Count; i++)
|
||
{
|
||
var item = items[i];
|
||
if (item.AlreadyUIShow) continue;
|
||
item.AlreadyUIShow = true;
|
||
newCount++;
|
||
|
||
// 解析发言者名称
|
||
var memberInfo = lobby.GetMemberInfo(item.SenderId);
|
||
string senderName = memberInfo?.Name;
|
||
if (string.IsNullOrEmpty(senderName))
|
||
senderName = $"玩家{item.SenderId}";
|
||
|
||
bool isSelf = item.SenderId == selfId;
|
||
|
||
var data = new ChatMessageData
|
||
{
|
||
SenderName = senderName,
|
||
Message = item.Message,
|
||
IsSelf = isSelf,
|
||
IsSystem = false,
|
||
Timestamp = DateTime.Now
|
||
};
|
||
CreateChatRow(data);
|
||
}
|
||
if (newCount > 0)
|
||
AudioManager.Instance.PlayAudio("SFX/UI_message");
|
||
Debug.Log($"[ChatArea] RefreshChatBox: {newCount} new messages, total items={items.Count}, rows={_chatRows.Count}");
|
||
}
|
||
|
||
private void RefreshChatContent()
|
||
{
|
||
RefreshChatBox();
|
||
if (DocArea != null && DocArea.activeSelf)
|
||
RefreshDocText();
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Internal
|
||
// ------------------------------------------------------------------
|
||
|
||
private void ToggleInputArea()
|
||
{
|
||
SetInputAreaVisible(!_inputAreaVisible);
|
||
}
|
||
|
||
private void SetInputAreaVisible(bool visible)
|
||
{
|
||
_inputAreaVisible = visible;
|
||
if (ChatInputArea != null)
|
||
ChatInputArea.SetActive(visible);
|
||
|
||
if (visible && TextInputArea != null)
|
||
TextInputArea.ActivateInputField();
|
||
}
|
||
|
||
private void ToggleDocArea()
|
||
{
|
||
if (DocArea == null) return;
|
||
bool willShow = !DocArea.activeSelf;
|
||
SetDocAreaVisible(willShow);
|
||
}
|
||
|
||
private void SetDocAreaVisible(bool visible)
|
||
{
|
||
if (DocArea != null)
|
||
DocArea.SetActive(visible);
|
||
if (visible)
|
||
RefreshDocText();
|
||
}
|
||
|
||
private void RefreshDocText()
|
||
{
|
||
if (DocText == null) return;
|
||
|
||
var items = ChatManager.Instance.ChatItems;
|
||
var lobby = LobbyManager.Instance?.Lobby;
|
||
|
||
int total = items.Count;
|
||
int start = Mathf.Max(0, total - DocRecentCount);
|
||
|
||
var sb = new System.Text.StringBuilder();
|
||
for (int i = start; i < total; i++)
|
||
{
|
||
var item = items[i];
|
||
string senderName = null;
|
||
if (lobby != null)
|
||
{
|
||
var memberInfo = lobby.GetMemberInfo(item.SenderId);
|
||
senderName = memberInfo?.Name;
|
||
}
|
||
if (string.IsNullOrEmpty(senderName))
|
||
senderName = $"玩家{item.SenderId}";
|
||
|
||
if (sb.Length > 0) sb.Append('\n');
|
||
sb.Append(FilterNameText(senderName)).Append(':').Append(FilterChatText(item.Message));
|
||
}
|
||
|
||
DocText.text = sb.ToString();
|
||
}
|
||
|
||
private void OnSendButtonClick()
|
||
{
|
||
TrySendMessage();
|
||
}
|
||
|
||
private void OnInputSubmit(string _)
|
||
{
|
||
TrySendMessage();
|
||
}
|
||
|
||
private void TrySendMessage()
|
||
{
|
||
if (TextInputArea == null) return;
|
||
string text = TextInputArea.text;
|
||
if (string.IsNullOrWhiteSpace(text)) return;
|
||
|
||
OnSendMessage?.Invoke(text.Trim());
|
||
TextInputArea.text = string.Empty;
|
||
TextInputArea.ActivateInputField();
|
||
}
|
||
|
||
private void CreateChatRow(ChatMessageData data)
|
||
{
|
||
if (ChatPrefab == null || ChatShowArea == null)
|
||
{
|
||
Debug.LogWarning($"[ChatArea] CreateChatRow failed: ChatPrefab={ChatPrefab != null}, ChatShowArea={ChatShowArea != null}");
|
||
return;
|
||
}
|
||
|
||
// 超出上限,移除最早的消息
|
||
while (_chatRows.Count >= MaxChatCount)
|
||
{
|
||
var oldest = _chatRows[0];
|
||
_chatRows.RemoveAt(0);
|
||
if (oldest.Go != null)
|
||
Destroy(oldest.Go);
|
||
}
|
||
|
||
var go = Instantiate(ChatPrefab, ChatShowArea);
|
||
|
||
// 拼接显示文本: "发言者:内容" 或系统消息直接显示内容
|
||
string displayMessage = FilterChatText(data.Message);
|
||
string displaySenderName = FilterNameText(data.SenderName);
|
||
string displayText = data.IsSystem
|
||
? displayMessage
|
||
: $"{displaySenderName}:{displayMessage}";
|
||
|
||
// 优先查找子物体上的 TMP,其次根物体
|
||
var tmp = go.GetComponentInChildren<TMP_Text>();
|
||
if (tmp != null)
|
||
{
|
||
tmp.text = displayText;
|
||
}
|
||
|
||
_chatRows.Add(new ChatRowEntry { Go = go, SpawnTime = Time.time });
|
||
}
|
||
|
||
private static string FilterChatText(string text)
|
||
{
|
||
return BannedWordFilter.Filter(text, BannedTextContext.Chat);
|
||
}
|
||
|
||
private static string FilterNameText(string text)
|
||
{
|
||
return BannedWordFilter.Filter(text, BannedTextContext.Name);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单条聊天消息数据
|
||
/// </summary>
|
||
public class ChatMessageData
|
||
{
|
||
public string SenderName;
|
||
public string Message;
|
||
public bool IsSelf;
|
||
public bool IsSystem;
|
||
public DateTime Timestamp;
|
||
}
|
||
}
|