diff --git a/Assets/PerfectWorld/Prefab/UIManager.prefab b/Assets/PerfectWorld/Prefab/UIManager.prefab
index 2443099e56..412f0b8f74 100644
--- a/Assets/PerfectWorld/Prefab/UIManager.prefab
+++ b/Assets/PerfectWorld/Prefab/UIManager.prefab
@@ -1522,7 +1522,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 1}
m_AnchorMax: {x: 0.5, y: 1}
- m_AnchoredPosition: {x: -187.8, y: -47.6}
+ m_AnchoredPosition: {x: -187.8, y: -47.600098}
m_SizeDelta: {x: 191, y: 60}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6965363531247653623
@@ -4825,6 +4825,7 @@ RectTransform:
m_Children:
- {fileID: 8741686998992894603}
- {fileID: 3488899534283412697}
+ - {fileID: 4668526399359599056}
- {fileID: 9056141770234008732}
- {fileID: 6541409353547558602}
- {fileID: 2907261990866691440}
@@ -6262,6 +6263,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
inputField: {fileID: 9217902013627304316}
chatSystem: {fileID: 0}
+ _spriteMap: {fileID: 0}
channelButtons: []
--- !u!1 &3544484534608324905
GameObject:
@@ -8700,8 +8702,8 @@ MonoBehaviour:
- {fileID: 2971821658315981769}
- {fileID: 452969679978752531}
- {fileID: 3647934876571221831}
- HpItemButton: {fileID: 7382895648940793749}
- MpItemButton: {fileID: 7614763976671640739}
+ HpItemButton: {fileID: 0}
+ MpItemButton: {fileID: 0}
m_nCurPanel1: 1
m_nCurPanel2: 1
m_bShowAll1: 0
@@ -10710,7 +10712,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0.5}
m_AnchorMax: {x: 0, y: 0.5}
- m_AnchoredPosition: {x: 60.559204, y: -126.849976}
+ m_AnchoredPosition: {x: 60.559204, y: -126.8501}
m_SizeDelta: {x: 101.855, y: 57.3622}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3863452661654338700
@@ -11648,7 +11650,6 @@ MonoBehaviour:
_btnTaskTrace: {fileID: 3253955040536933532}
_taskTraceParent: {fileID: 2578159539417438268}
_btnTeamList: {fileID: 540188344648694736}
- _lockviewList: {fileID: 0}
--- !u!1 &7352847439676120744
GameObject:
m_ObjectHideFlags: 0
@@ -18712,7 +18713,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 8435310359341866937, guid: b5a4a3ed5bf0e5a49ba0f89d26e1f36e, type: 3}
propertyPath: m_AnchoredPosition.y
- value: -25.335571
+ value: -25.33545
objectReference: {fileID: 0}
- target: {fileID: 8579427623307909814, guid: b5a4a3ed5bf0e5a49ba0f89d26e1f36e, type: 3}
propertyPath: m_AnchorMax.y
@@ -18948,6 +18949,14 @@ PrefabInstance:
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
+ - target: {fileID: 2621697629504226575, guid: a531b4b63ab8355419f297fe10d32abe, type: 3}
+ propertyPath: miniChatContent
+ value:
+ objectReference: {fileID: 3171192075315926561}
+ - target: {fileID: 2621697629504226575, guid: a531b4b63ab8355419f297fe10d32abe, type: 3}
+ propertyPath: onOpenChatPanelButton
+ value:
+ objectReference: {fileID: 1884013949825403952}
- target: {fileID: 2818704151482351807, guid: a531b4b63ab8355419f297fe10d32abe, type: 3}
propertyPath: m_AnchorMax.y
value: 0
@@ -20714,6 +20723,124 @@ RectTransform:
m_CorrespondingSourceObject: {fileID: 4475312012745311020, guid: c82978c3789dad44da354dc354c782b2, type: 3}
m_PrefabInstance: {fileID: 8244659259478137406}
m_PrefabAsset: {fileID: 0}
+--- !u!1001 &8343482093857271211
+PrefabInstance:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 3233441867675090637}
+ m_Modifications:
+ - target: {fileID: 843061721837273778, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_Name
+ value: prefab_MiniChat
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_Pivot.x
+ value: 0.5
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_Pivot.y
+ value: 0.5
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_AnchorMax.x
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_AnchorMax.y
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_AnchorMin.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_AnchorMin.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_SizeDelta.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_SizeDelta.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_AnchoredPosition.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_AnchoredPosition.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ m_RemovedComponents: []
+ m_RemovedGameObjects: []
+ m_AddedGameObjects: []
+ m_AddedComponents: []
+ m_SourcePrefab: {fileID: 100100000, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+--- !u!114 &1884013949825403952 stripped
+MonoBehaviour:
+ m_CorrespondingSourceObject: {fileID: 7633421558195200411, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ m_PrefabInstance: {fileID: 8343482093857271211}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+--- !u!224 &3171192075315926561 stripped
+RectTransform:
+ m_CorrespondingSourceObject: {fileID: 6901861700741151626, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ m_PrefabInstance: {fileID: 8343482093857271211}
+ m_PrefabAsset: {fileID: 0}
+--- !u!224 &4668526399359599056 stripped
+RectTransform:
+ m_CorrespondingSourceObject: {fileID: 3676046446549609595, guid: 05206a01b3910384cb9a17c74225d554, type: 3}
+ m_PrefabInstance: {fileID: 8343482093857271211}
+ m_PrefabAsset: {fileID: 0}
--- !u!1001 &8508566894086459119
PrefabInstance:
m_ObjectHideFlags: 0
diff --git a/Assets/PerfectWorld/ScriptableObjects/ChatSystems/EmotionLibrarySpriteMap.asset b/Assets/PerfectWorld/ScriptableObjects/ChatSystems/EmotionLibrarySpriteMap.asset
index c873bc9b2b..de5f612de4 100644
--- a/Assets/PerfectWorld/ScriptableObjects/ChatSystems/EmotionLibrarySpriteMap.asset
+++ b/Assets/PerfectWorld/ScriptableObjects/ChatSystems/EmotionLibrarySpriteMap.asset
@@ -12,7 +12,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: af2fa24fb63c4aa45bb99a711c857114, type: 3}
m_Name: EmotionLibrarySpriteMap
m_EditorClassIdentifier:
- Library: {fileID: 11400000, guid: 3011939e4e9c0ce4e83bc03a748fcf96, type: 2}
- TmpSpriteAsset: {fileID: 11400000, guid: c41005c129ba4d66911b75229fd70b45, type: 2}
+ Library: {fileID: 11400000, guid: e4fbe1473e80a9c4794327d8bb11a4ab, type: 2}
+ TmpSpriteAsset: {fileID: 11400000, guid: ee97404ddf0b24345809ec3b36f78e02, type: 2}
PreferSpriteNameTag: 1
DefaultAnimFps: 10
diff --git a/Assets/PerfectWorld/Scripts/Chat/ChatEmotionDisplayPipeline.cs b/Assets/PerfectWorld/Scripts/Chat/ChatEmotionDisplayPipeline.cs
index 6224c4e22c..1838fd338e 100644
--- a/Assets/PerfectWorld/Scripts/Chat/ChatEmotionDisplayPipeline.cs
+++ b/Assets/PerfectWorld/Scripts/Chat/ChatEmotionDisplayPipeline.cs
@@ -1,4 +1,6 @@
+using System.Text.RegularExpressions;
using CSNetwork;
+using UnityEngine;
namespace BrewMonster.Scripts.Chat
{
@@ -8,16 +10,44 @@ namespace BrewMonster.Scripts.Chat
///
public sealed class ChatEmotionDisplayPipeline
{
+ ///
+ /// Khi thiếu ký tự PUA trước payload, Unmarshal không chạy nhưng vẫn còn <0><set:index> (Serialize).
+ /// When PUA prefix is missing, Unmarshal skips but <0><set:index> (Serialize) may remain.
+ ///
+ private static readonly Regex LooseMarshaledEmotionRegex = new Regex(
+ @"<0><(\d+):(\d+)>",
+ RegexOptions.Compiled);
+
private IEmotionSpriteMap _spriteMap = new StubEmotionSpriteMap();
+ ///
+ /// Wire 正文(服务器 / 输入缓冲)→ TMP 显示用富文本 — 与 CECGameUIMan.AddChatMessage 内转换一致(不含 FilterBadWords)。
+ /// Wire body (server / input buffer) → TMP rich text for display — same as CECGameUIMan.AddChatMessage (without FilterBadWords).
+ ///
+ public static string ConvertWireBodyToTmpDisplay(string wireBody, IEmotionSpriteMap map, int cEmotion)
+ {
+ if (string.IsNullOrEmpty(wireBody))
+ return "";
+ if (map == null)
+ return wireBody;
+
+ var p = new ChatEmotionDisplayPipeline(map);
+ string filtered = p.ApplyChannelEmotionFilter(wireBody, cEmotion);
+ return p.ConvertInlineItemsToTmp(filtered);
+ }
+
public ChatEmotionDisplayPipeline(IEmotionSpriteMap spriteMap = null)
{
if (spriteMap != null)
_spriteMap = spriteMap;
+ else
+ BMLogger.LogWarning("[ChatEmotionDisplayPipeline] spriteMap is null — using StubEmotionSpriteMap. Call SetSpriteMap() or assign EmotionLibrarySpriteMap SO on CECUIManager.");
}
public void SetSpriteMap(IEmotionSpriteMap spriteMap)
{
+ if (spriteMap == null)
+ BMLogger.LogWarning("[ChatEmotionDisplayPipeline] SetSpriteMap() called with null — EmotionLibrarySpriteMap SO chưa được gán trên CECUIManager Inspector.");
_spriteMap = spriteMap ?? new StubEmotionSpriteMap();
}
@@ -35,15 +65,38 @@ namespace BrewMonster.Scripts.Chat
{
EditBoxItemsSet itemsSet = new EditBoxItemsSet();
string displayText = AUICommon.UnmarshalEditBoxText(pszMsgAfterBadWordFilter, itemsSet);
+ string tmpText;
if (itemsSet.GetItemCount() <= 0)
- return pszMsgAfterBadWordFilter;
+ tmpText = pszMsgAfterBadWordFilter;
+ else
+ {
+ tmpText = displayText;
+ tmpText = AUICommon.ConvertEmotionsToTMP(tmpText, itemsSet, _spriteMap);
+ tmpText = AUICommon.ConvertCoordsToTMP(tmpText, itemsSet);
+ tmpText = AUICommon.ConvertIvtrItemsToTMP(tmpText, itemsSet);
+ tmpText = AUICommon.StripRemainingItemCodes(tmpText);
+ }
- string tmpText = displayText;
- tmpText = AUICommon.ConvertEmotionsToTMP(tmpText, itemsSet, _spriteMap);
- tmpText = AUICommon.ConvertCoordsToTMP(tmpText, itemsSet);
- tmpText = AUICommon.ConvertIvtrItemsToTMP(tmpText, itemsSet);
- tmpText = AUICommon.StripRemainingItemCodes(tmpText);
- return tmpText;
+ return ReplaceLooseMarshaledEmotionTags(tmpText);
+ }
+
+ ///
+ /// Fallback: wire emotion chỉ còn dạng Serialize <0><set:index> (không qua PUA) → TMP <sprite>.
+ /// Fallback: emotion wire as Serialize-only <0><set:index> (no PUA) → TMP <sprite>.
+ ///
+ private string ReplaceLooseMarshaledEmotionTags(string text)
+ {
+ if (string.IsNullOrEmpty(text) || _spriteMap == null)
+ return text;
+
+ return LooseMarshaledEmotionRegex.Replace(text, match =>
+ {
+ int set = int.Parse(match.Groups[1].Value);
+ int index = int.Parse(match.Groups[2].Value);
+ if (_spriteMap.TryGetSprite(set, index, out EmotionSpriteInfo info))
+ return EmotionTMPTagBuilder.BuildSpriteTag(info);
+ return match.Value;
+ });
}
}
}
diff --git a/Assets/PerfectWorld/Scripts/Chat/ChatWireTmpCodec.cs b/Assets/PerfectWorld/Scripts/Chat/ChatWireTmpCodec.cs
new file mode 100644
index 0000000000..5b510f63cc
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Chat/ChatWireTmpCodec.cs
@@ -0,0 +1,100 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using CSNetwork;
+
+namespace BrewMonster.Scripts.Chat
+{
+ ///
+ /// Chat 输入/协议:wire(MarshalEditBoxText)↔ TMP <sprite> 显示。
+ /// Chat input/protocol: wire (MarshalEditBoxText) ↔ TMP <sprite> display.
+ ///
+ public static class ChatWireTmpCodec
+ {
+ private static readonly Regex SpriteTagRegex = new Regex(@"]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ ///
+ /// Tạo một đoạn wire marshal cho một emotion (set:index) — gửi server đúng protocol.
+ /// Build one marshaled wire segment for a single emotion (set:index) — correct server protocol.
+ ///
+ public static string BuildMarshaledEmotionWire(int emotionSet, int emotionIndex)
+ {
+ var item = new EditBoxItemBase(AUICommon.EditboxItemType.enumEIEmotion);
+ item.SetName("W");
+ item.SetInfo(AUICommon.MarshalEmotionInfo(emotionSet, emotionIndex));
+ var items = new EditBoxItemsSet();
+ char c = items.AppendItem(item);
+ if (c == '\0')
+ return "";
+
+ string display = c.ToString();
+ return AUICommon.MarshalEditBoxText(display, items);
+ }
+
+ ///
+ /// TMP 正文(无频道前缀)→ wire marshal(用于发送)。
+ /// TMP body text (no channel prefix) → marshaled wire (for sending).
+ ///
+ public static string TmpBodyToWire(string tmpBody, IEmotionSpriteMap map)
+ {
+ if (string.IsNullOrEmpty(tmpBody))
+ return "";
+ if (map == null)
+ return tmpBody;
+
+ var sb = new StringBuilder(tmpBody.Length);
+ int last = 0;
+ foreach (Match m in SpriteTagRegex.Matches(tmpBody))
+ {
+ sb.Append(tmpBody, last, m.Index - last);
+ string tag = m.Value;
+ if (TryMatchSpriteTagToEmotion(map, tag, out int es, out int ei))
+ sb.Append(BuildMarshaledEmotionWire(es, ei));
+ else
+ sb.Append(tag);
+ last = m.Index + m.Length;
+ }
+
+ sb.Append(tmpBody, last, tmpBody.Length - last);
+ return sb.ToString();
+ }
+
+ ///
+ /// Khớp tag với EmotionTMPTagBuilder — duyệt (set,index) đủ nhỏ.
+ /// Match tag to EmotionTMPTagBuilder output — brute-force over (set,index) within reasonable bounds.
+ ///
+ public static bool TryMatchSpriteTagToEmotion(IEmotionSpriteMap map, string spriteTag, out int emotionSet, out int emotionIndex)
+ {
+ emotionSet = 0;
+ emotionIndex = 0;
+ if (map == null || string.IsNullOrEmpty(spriteTag))
+ return false;
+
+ string normalized = spriteTag.Trim();
+ for (int s = 0; s < AUICommon.AUIMANAGER_MAX_EMOTIONGROUPS; s++)
+ {
+ for (int e = 0; e < 512; e++)
+ {
+ if (!EmotionTMPTagBuilder.TryBuildEmotionTag(map, s, e, out string built))
+ continue;
+ if (built == normalized)
+ {
+ emotionSet = s;
+ emotionIndex = e;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Wire → TMP 富文本(FilterEmotionSet + Unmarshal + ConvertInlineItemsToTmp)。
+ /// Wire → TMP rich text (FilterEmotionSet + Unmarshal + ConvertInlineItemsToTmp).
+ ///
+ public static string WireBodyToTmpForDisplay(string wireBody, IEmotionSpriteMap map, int cEmotion)
+ {
+ return ChatEmotionDisplayPipeline.ConvertWireBodyToTmpDisplay(wireBody, map, cEmotion);
+ }
+ }
+}
diff --git a/Assets/PerfectWorld/Scripts/Chat/ChatWireTmpCodec.cs.meta b/Assets/PerfectWorld/Scripts/Chat/ChatWireTmpCodec.cs.meta
new file mode 100644
index 0000000000..6ac771ec84
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Chat/ChatWireTmpCodec.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9a9d1336ec810654dae6b1bb4076ae3a
\ No newline at end of file
diff --git a/Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySO.cs b/Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySO.cs
index 2459b04feb..6b7760cf07 100644
--- a/Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySO.cs
+++ b/Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySO.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using TMPro;
using UnityEngine;
namespace BrewMonster.Scripts.Chat.EmotionData
@@ -16,6 +17,11 @@ namespace BrewMonster.Scripts.Chat.EmotionData
public int CellHeight = 32;
public Texture2D SourceAtlas;
public string SourceTxtAssetPath = "";
+
+ [Tooltip("TMP_SpriteAsset tương ứng với atlas này (cell_0000…). Bắt buộc cho emoji động (nhiều frame). " +
+ "TMP_SpriteAsset matching this atlas (cell_0000…). Required for animated emoji (multi-frame).")]
+ public TMP_SpriteAsset TmpSpriteAsset;
+
public List Entries = new List();
}
diff --git a/Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySpriteMap.cs b/Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySpriteMap.cs
index 90b6f7477a..508683a1e8 100644
--- a/Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySpriteMap.cs
+++ b/Assets/PerfectWorld/Scripts/Chat/EmotionData/EmotionLibrarySpriteMap.cs
@@ -1,120 +1,134 @@
-using System;
-using TMPro;
using UnityEngine;
namespace BrewMonster.Scripts.Chat.EmotionData
{
///
/// Ánh xạ (emotionSet, emotionIndex) từ sang tag TMP.
- /// Gán asset này vào (field emotion) — không phải MonoBehaviour nên không kéo SO trên Inspector được.
+ /// Maps (emotionSet, emotionIndex) from EmotionLibrarySO to TMP rich-text tag.
+ ///
+ /// Atlas được cắt trái→phải, trên→dưới, sub-sprite đặt tên cell_XXXX.
+ /// Số XXXX trong tên chính là index tuyến tính → không cần tra spriteCharacterTable.
+ /// Atlas is sliced left-to-right, top-to-bottom, named cell_XXXX.
+ /// The number XXXX in the name IS the linear index → no spriteCharacterTable lookup needed.
///
/// Cách dùng:
- /// 1) Tạo bằng Emotion Atlas Converter.
- /// 2) Tạo từ cùng atlas (Window → TextMeshPro → Sprite Importer),
- /// đảm bảo tên sub-sprite khớp (vd. cell_0000) với atlas Multiple từ tool.
- /// 3) Gán TMP_SpriteAsset vào ô chat (Sprite Asset / Additional).
- /// 4) Kéo SO này vào field trên GameObject có (Awake gọi SetEmotionSpriteMap).
+ /// 1) Tạo EmotionLibrarySO bằng Emotion Atlas Converter.
+ /// 2) Gán TMP_SpriteAsset cho từng set vào EmotionSetSnapshot.TmpSpriteAsset trong Library
+ /// (chỉ cần cho emoji động nhiều frame hoặc khi PreferSpriteNameTag = false).
+ /// 3) Kéo SO này vào field trên GameObject có CECUIManager (Awake gọi SetEmotionSpriteMap).
///
[CreateAssetMenu(fileName = "EmotionLibrarySpriteMap", menuName = "Perfect World/Chat/Emotion Library Sprite Map", order = 2)]
public class EmotionLibrarySpriteMap : ScriptableObject, IEmotionSpriteMap
{
- [Tooltip("Dữ liệu emotion đã convert (Entries + FrameSprites).")]
+ [Tooltip("Dữ liệu emotion đã convert. Emotion data converted by the atlas tool.")]
public EmotionLibrarySO Library;
- [Tooltip("Sprite Asset dùng cho chat TMP — cùng tên sub-sprite với atlas (cell_XXXX). Bắt buộc cho emoji động (anim) hoặc khi không dùng name tag.")]
- public TMP_SpriteAsset TmpSpriteAsset;
-
- [Tooltip("Nếu true và chỉ 1 frame: thử (không cần tra index). Động (nhiều frame) vẫn cần TmpSpriteAsset để tra index.")]
+ [Tooltip("Nếu true và emoji chỉ 1 frame: dùng thay vì . " +
+ "If true and single-frame: use instead of .")]
public bool PreferSpriteNameTag = true;
- [Tooltip("FPS mặc định cho khi không suy ra được từ FrameTicks.")]
+ [Tooltip("FPS mặc định cho khi không suy ra được từ FrameTicks. " +
+ "Default FPS for when it cannot be derived from FrameTicks.")]
public int DefaultAnimFps = 10;
+ // ─────────────────────────────────────────────────────────────────────
+ // IEmotionSpriteMap
+ // ─────────────────────────────────────────────────────────────────────
+
public bool TryGetSprite(int emotionSet, int emotionIndex, out EmotionSpriteInfo info)
{
info = default;
if (Library == null)
+ {
+ Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: Library == null. Gán EmotionLibrarySO vào field Library.");
return false;
+ }
EmotionSetSnapshot set = Library.GetSetOrNull(emotionSet);
- if (set == null || emotionIndex < 0 || emotionIndex >= set.Entries.Count)
+ if (set == null)
+ {
+ Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: Không tìm thấy set index={emotionSet}. " +
+ $"Sets hiện có: [{string.Join(", ", Library.Sets?.ConvertAll(s => s?.EmotionSetIndex.ToString() ?? "null") ?? new System.Collections.Generic.List())}]");
return false;
+ }
+
+ if (emotionIndex < 0 || emotionIndex >= set.Entries.Count)
+ {
+ Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: emotionIndex={emotionIndex} ngoài range [0, {set.Entries.Count - 1}].");
+ return false;
+ }
EmotionEntryData entry = set.Entries[emotionIndex];
Sprite[] frames = entry.FrameSprites;
if (frames == null || frames.Length == 0)
- return false;
-
- if (frames.Length == 1)
{
- Sprite s0 = frames[0];
- if (s0 == null)
- return false;
+ Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: entry.FrameSprites null/empty. Chạy lại Emotion Atlas Converter để populate FrameSprites.");
+ return false;
+ }
- if (PreferSpriteNameTag && !string.IsNullOrEmpty(s0.name))
+ // Dùng StartPos/NumFrames từ txt (khớp EmotionAtlasConverterCore: cellIndex = StartPos + f).
+ // Use StartPos/NumFrames from txt (matches EmotionAtlasConverterCore: cellIndex = StartPos + f).
+ // Không đọc Sprite.name — GetName() chỉ được gọi trên main thread (vd. gói chat từ network thread).
+ // Do not read Sprite.name — GetName() is main-thread-only (e.g. chat packets on network thread).
+ int startCell = entry.StartPos;
+ int frameCount = frames.Length;
+ if (startCell < 0 || frameCount < 1)
+ {
+ Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: StartPos={startCell} hoặc không có frame.");
+ return false;
+ }
+
+ // ── Single frame ──────────────────────────────────────────────────
+ if (frameCount == 1)
+ {
+ if (frames[0] == null)
{
- info = new EmotionSpriteInfo
- {
- UseSpriteName = true,
- SpriteName = s0.name,
- IsAnimated = false
- };
+ Debug.LogWarning($"[EmotionLibrarySpriteMap] TryGetSprite({emotionSet},{emotionIndex}) → FAIL: frames[0] == null.");
+ return false;
+ }
+
+ if (PreferSpriteNameTag)
+ {
+ // — TMP tra tên trong SpriteAsset được gán trên component.
+ // — TMP looks up the name in the SpriteAsset on the component.
+ info = new EmotionSpriteInfo { UseSpriteName = true, SpriteName = FormatCellSpriteName(startCell), IsAnimated = false };
return true;
}
- int idx = FindSpriteIndexInTmpAsset(s0.name);
- if (idx < 0)
- return false;
-
- info = new EmotionSpriteInfo { SpriteIndex = idx, IsAnimated = false };
+ info = new EmotionSpriteInfo { SpriteIndex = startCell, IsAnimated = false };
return true;
}
- if (TmpSpriteAsset == null)
- return false;
+ // ── Multi-frame (animated) ────────────────────────────────────────
+ int endCell = startCell + frameCount - 1;
- int start = FindSpriteIndexInTmpAsset(frames[0]?.name);
- int end = FindSpriteIndexInTmpAsset(frames[frames.Length - 1]?.name);
- if (start < 0 || end < 0)
- return false;
-
- int fps = EstimateFps(entry.FrameTicks, frames.Length);
+ int fps = EstimateFps(entry.FrameTicks, frameCount);
info = new EmotionSpriteInfo
{
- SpriteIndex = start,
- IsAnimated = true,
- AnimEndFrame = end,
- AnimFPS = fps > 0 ? fps : DefaultAnimFps
+ SpriteIndex = startCell,
+ IsAnimated = true,
+ AnimEndFrame = endCell,
+ AnimFPS = fps > 0 ? fps : DefaultAnimFps
};
return true;
}
- private int FindSpriteIndexInTmpAsset(string spriteName)
- {
- if (TmpSpriteAsset == null || string.IsNullOrEmpty(spriteName))
- return -1;
+ // ─────────────────────────────────────────────────────────────────────
+ // Helpers
+ // ─────────────────────────────────────────────────────────────────────
- if (TmpSpriteAsset.spriteInfoList != null)
- {
- for (int i = 0; i < TmpSpriteAsset.spriteInfoList.Count; i++)
- {
- var s = TmpSpriteAsset.spriteInfoList[i];
- if (s != null && s.name == spriteName)
- return i;
- }
- }
-
- return -1;
- }
+ ///
+ /// Tên ô giống tool atlas: "cell_XXXX" (4 chữ số). Không đọc Unity Sprite.name.
+ /// Same cell naming as atlas tool: "cell_XXXX" (4 digits). Does not read Unity Sprite.name.
+ ///
+ private static string FormatCellSpriteName(int cellIndex) => $"cell_{cellIndex:D4}";
private static int EstimateFps(int[] frameTicks, int numFrames)
{
if (frameTicks == null || numFrames < 2 || frameTicks.Length < numFrames)
return 0;
- int last = frameTicks[numFrames - 1];
- int first = frameTicks[0];
- int span = last - first;
+ int span = frameTicks[numFrames - 1] - frameTicks[0];
if (span <= 0)
return 0;
return Mathf.Max(1, Mathf.RoundToInt((numFrames - 1) * 60f / span));
diff --git a/Assets/PerfectWorld/Scripts/Chat/EmotionTMPTagBuilder.cs b/Assets/PerfectWorld/Scripts/Chat/EmotionTMPTagBuilder.cs
new file mode 100644
index 0000000000..d5ca589b56
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Chat/EmotionTMPTagBuilder.cs
@@ -0,0 +1,156 @@
+using TMPro;
+using UnityEngine;
+
+namespace BrewMonster.Scripts.Chat
+{
+ ///
+ /// Helper tĩnh: chuyển EmotionSpriteInfo → TMP rich-text tag và insert vào TMP_InputField.
+ /// Static helper: converts EmotionSpriteInfo → TMP rich-text tag and inserts it into a TMP_InputField.
+ ///
+ /// Cách dùng điển hình / Typical usage:
+ ///
+ /// // Khi người dùng bấm chọn emoji từ picker:
+ /// // When user taps an emoji in the picker:
+ /// EmotionTMPTagBuilder.InsertEmotionTag(_chatInputField, _spriteMap, emotionSet, emotionIndex);
+ ///
+ /// // Hoặc chỉ lấy tag string rồi tự xử lý:
+ /// // Or just get the tag string and handle it yourself:
+ /// if (EmotionTMPTagBuilder.TryBuildEmotionTag(_spriteMap, set, idx, out string tag))
+ /// Debug.Log(tag); // → "" hoặc ""
+ ///
+ ///
+ public static class EmotionTMPTagBuilder
+ {
+ // ─────────────────────────────────────────────────────────────────────
+ // Tag builders
+ // ─────────────────────────────────────────────────────────────────────
+
+ ///
+ /// Tạo TMP rich-text tag từ EmotionSpriteInfo đã resolve.
+ /// Build TMP rich-text tag from an already-resolved EmotionSpriteInfo.
+ ///
+ /// Thứ tự ưu tiên / Priority:
+ /// 1. UseSpriteName → <sprite name="…">
+ /// 2. IsAnimated → <sprite anim="start,end,fps">
+ /// 3. (fallback) → <sprite index=N>
+ ///
+ public static string BuildSpriteTag(EmotionSpriteInfo info)
+ {
+ if (info.UseSpriteName && !string.IsNullOrEmpty(info.SpriteName))
+ return $"";
+
+ if (info.IsAnimated)
+ return $"";
+
+ return $"";
+ }
+
+ ///
+ /// Tra bảng spriteMap rồi tạo TMP tag.
+ /// Lookup spriteMap then build the TMP tag.
+ ///
+ /// Bảng ánh xạ emotion → sprite. Emotion-to-sprite mapping.
+ /// Chỉ số bộ (N trong Emotions{N}). Emotion set index.
+ /// Chỉ số emotion trong bộ (dòng trong .txt). Emotion index within set.
+ /// TMP tag được tạo ra nếu thành công. Built TMP tag on success.
+ /// true nếu tạo được tag, false nếu không tìm thấy emotion. true if tag was built.
+ public static bool TryBuildEmotionTag(IEmotionSpriteMap spriteMap, int emotionSet, int emotionIndex, out string tag)
+ {
+ tag = string.Empty;
+ if (spriteMap == null)
+ {
+ Debug.LogWarning("[Cuong] TryBuildEmotionTag spriteMap is null.");
+ return false;
+ }
+
+ if (!spriteMap.TryGetSprite(emotionSet, emotionIndex, out EmotionSpriteInfo info))
+ {
+ Debug.LogWarning("[Cuong] TryBuildEmotionTag.");
+ return false;
+ }
+ tag = BuildSpriteTag(info);
+ return true;
+ }
+
+ // ─────────────────────────────────────────────────────────────────────
+ // TMP_InputField helpers
+ // ─────────────────────────────────────────────────────────────────────
+
+ ///
+ /// Insert TMP emotion tag vào TMP_InputField tại vị trí caret hiện tại.
+ /// Insert the TMP emotion tag into a TMP_InputField at the current caret position.
+ ///
+ /// InputField đang nhập chat. The active chat input field.
+ /// Bảng ánh xạ. Sprite map.
+ /// Chỉ số bộ. Emotion set index.
+ /// Chỉ số emotion. Emotion index within set.
+ /// true nếu tag đã được insert. true if the tag was inserted.
+ public static bool InsertEmotionTag(TMP_InputField inputField, IEmotionSpriteMap spriteMap, int emotionSet, int emotionIndex)
+ {
+ if (inputField == null)
+ {
+ BMLogger.LogWarning("[Cuong] InsertEmotionTag inputField is null.");
+ return false;
+ }
+
+ if (!TryBuildEmotionTag(spriteMap, emotionSet, emotionIndex, out string tag))
+ {
+ BMLogger.LogWarning($"[Cuong] TryBuildEmotionTag false {emotionSet} {emotionIndex} {tag}");
+ return false;
+ }
+
+ InsertTagAtCaret(inputField, tag);
+ return true;
+ }
+
+ ///
+ /// Insert tag tại vị trí caret, cập nhật caret sau khi insert.
+ /// Insert tag at caret position, advance caret after insertion.
+ ///
+ public static void InsertTagAtCaret(TMP_InputField inputField, string tag)
+ {
+ if (inputField == null || string.IsNullOrEmpty(tag))
+ return;
+
+ string text = inputField.text ?? string.Empty;
+ int caret = Mathf.Clamp(inputField.caretPosition, 0, text.Length);
+
+ inputField.text = text.Insert(caret, tag);
+
+ // Đặt caret sau tag vừa insert — Move caret to end of inserted tag
+ inputField.caretPosition = caret + tag.Length;
+ inputField.ForceLabelUpdate();
+ }
+
+ // ─────────────────────────────────────────────────────────────────────
+ // Diagnostics
+ // ─────────────────────────────────────────────────────────────────────
+
+ ///
+ /// Log toàn bộ emotion set/index có trong spriteMap ra Console — dùng debug.
+ /// Log all emotion set/index entries in spriteMap to Console — for debugging.
+ ///
+ public static void DebugLogAllEntries(IEmotionSpriteMap spriteMap, int maxSets = 10, int maxEmotionsPerSet = 50)
+ {
+ if (spriteMap == null)
+ {
+ Debug.LogWarning("[Cuong] DebugLogAllEntries spriteMap is null.");
+ return;
+ }
+
+ System.Text.StringBuilder sb = new System.Text.StringBuilder();
+ sb.AppendLine("[EmotionTMPTagBuilder] All entries:");
+
+ for (int s = 0; s < maxSets; s++)
+ {
+ for (int e = 0; e < maxEmotionsPerSet; e++)
+ {
+ if (spriteMap.TryGetSprite(s, e, out EmotionSpriteInfo info))
+ sb.AppendLine($" set={s} idx={e} → {BuildSpriteTag(info)}");
+ }
+ }
+
+ BMLogger.Log(sb.ToString());
+ }
+ }
+}
diff --git a/Assets/PerfectWorld/Scripts/Chat/EmotionTMPTagBuilder.cs.meta b/Assets/PerfectWorld/Scripts/Chat/EmotionTMPTagBuilder.cs.meta
new file mode 100644
index 0000000000..093e5d1af1
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Chat/EmotionTMPTagBuilder.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 200b07ddd8d07064b9e6896fda84dafe
\ No newline at end of file
diff --git a/Assets/PerfectWorld/Scripts/Chat/StubEmotionSpriteMap.cs b/Assets/PerfectWorld/Scripts/Chat/StubEmotionSpriteMap.cs
index b590680f08..757392a477 100644
--- a/Assets/PerfectWorld/Scripts/Chat/StubEmotionSpriteMap.cs
+++ b/Assets/PerfectWorld/Scripts/Chat/StubEmotionSpriteMap.cs
@@ -1,3 +1,5 @@
+using BrewMonster;
+
///
/// Stub实现 — 在没有真实TMP_SpriteAsset(emotion atlas)时使用的占位映射。
/// Stub implementation used when no real TMP_SpriteAsset (emotion atlas) is available yet.
@@ -15,7 +17,7 @@ public class StubEmotionSpriteMap : IEmotionSpriteMap
public bool TryGetSprite(int emotionSet, int emotionIndex, out EmotionSpriteInfo info)
{
int startFrame = (emotionSet * EMOTIONS_PER_SET + emotionIndex) * FRAMES_PER_EMOTION;
-
+ BMLogger.Log("[Cuong] TryGetSprite");
info = new EmotionSpriteInfo
{
SpriteIndex = startFrame,
diff --git a/Assets/PerfectWorld/Scripts/Chat/UI/ChatSystemlUI.cs b/Assets/PerfectWorld/Scripts/Chat/UI/ChatSystemlUI.cs
index 36532b0ed1..d2ac43f23e 100644
--- a/Assets/PerfectWorld/Scripts/Chat/UI/ChatSystemlUI.cs
+++ b/Assets/PerfectWorld/Scripts/Chat/UI/ChatSystemlUI.cs
@@ -17,21 +17,14 @@ namespace BrewMonster.Scripts.ChatUI
public class ChatSystemlUI : MonoBehaviour
{
- [Header("MiniChat")]
- public Button onOpenChatPanelButton;
- [Tooltip("Parent cho các dòng tin xem trước (nên có VerticalLayoutGroup).")]
- public RectTransform miniChatContent;
- [Tooltip("Null = dùng messagePrefab.")]
- public ChatMessageView miniMessagePrefab;
- [SerializeField] int miniChatPreviewLines = 5;
- [Tooltip("Tắt raycast trên dòng preview để click xuyên xuống onOpenChatPanelButton (TMP/Image mặc định chặn Button).")]
- [SerializeField] bool miniChatPassThroughClicks = true;
-
[Header("ChatPanelUI")] public ScrollRect scrollRect;
+ [Tooltip("Nền/fullscreen block — bật/tắt cùng lúc với chatPanelUIGO (có thể để null).")]
+ public GameObject BgGameObject;
public GameObject chatPanelUIGO;
public RectTransform content;
public ChatMessageView messagePrefab;
public Button closeChatPanelButton;
+ public Button closeBGChatPanelButton;
public Button emojiButton;
public Button sendButton;
@@ -49,9 +42,6 @@ namespace BrewMonster.Scripts.ChatUI
private List _messages = new();
private List _visibleViews = new();
private List _filteredMessagesCache = new();
- private readonly List _miniFilterBuffer = new();
- private readonly List _miniChatViews = new();
-
private ObjectPool _pool;
private bool _userAtBottom = true;
@@ -74,6 +64,7 @@ namespace BrewMonster.Scripts.ChatUI
EventBus.Subscribe(OnChatMessageClear);
EventBus.Subscribe(OnChatMessageReceived);
EventBus.Subscribe(OnChannelFilterChanged);
+ EventBus.Subscribe(OnOpenChatPanelRequested);
_pool = new ObjectPool(
CreateItem,
OnGetItem,
@@ -86,13 +77,20 @@ namespace BrewMonster.Scripts.ChatUI
scrollRect.onValueChanged.AddListener(OnScrollChanged);
+ SetChatPanelAndBgVisible(false);
+ }
+
+ /// Bật/tắt panel chat và BG cùng trạng thái (2 GO tách nhưng luồng giống nhau).
+ void SetChatPanelAndBgVisible(bool visible)
+ {
if (chatPanelUIGO != null)
- chatPanelUIGO.SetActive(false);
+ chatPanelUIGO.SetActive(visible);
+ if (BgGameObject != null)
+ BgGameObject.SetActive(visible);
}
void OnEnable()
{
- RefreshMiniChat();
if (chatPanelUIGO == null || !chatPanelUIGO.activeSelf || _pool == null || content == null)
return;
@@ -107,10 +105,10 @@ namespace BrewMonster.Scripts.ChatUI
void WireUiButtons()
{
- if (onOpenChatPanelButton != null)
- onOpenChatPanelButton.onClick.AddListener(OpenChatPanel);
if (closeChatPanelButton != null)
closeChatPanelButton.onClick.AddListener(CloseChatPanel);
+ if (closeBGChatPanelButton != null)
+ closeBGChatPanelButton.onClick.AddListener(CloseChatPanel);
if (emojiButton != null)
emojiButton.onClick.AddListener(ToggleEmojiPanel);
if (closeEmojiPanelButton != null)
@@ -121,10 +119,10 @@ namespace BrewMonster.Scripts.ChatUI
private void OnDestroy()
{
- if (onOpenChatPanelButton != null)
- onOpenChatPanelButton.onClick.RemoveListener(OpenChatPanel);
if (closeChatPanelButton != null)
closeChatPanelButton.onClick.RemoveListener(CloseChatPanel);
+ if (closeBGChatPanelButton != null)
+ closeBGChatPanelButton.onClick.RemoveListener(CloseChatPanel);
if (emojiButton != null)
emojiButton.onClick.RemoveListener(ToggleEmojiPanel);
if (closeEmojiPanelButton != null)
@@ -135,25 +133,29 @@ namespace BrewMonster.Scripts.ChatUI
EventBus.Unsubscribe(OnChatMessageClear);
EventBus.Unsubscribe(OnChatMessageReceived);
EventBus.Unsubscribe(OnChannelFilterChanged);
+ EventBus.Unsubscribe(OnOpenChatPanelRequested);
if (_pool != null)
{
_pool.Clear();
_pool.Dispose();
}
-
- ClearMiniChatViews();
}
private void OnChannelFilterChanged(ChatChannelFilterChangedEvent e)
{
if (this == null) return;
_currentFilterChannel = e.channel;
- RefreshMiniChat();
if (chatPanelUIGO != null && chatPanelUIGO.activeSelf)
RefreshVisible();
}
+ void OnOpenChatPanelRequested(OpenChatPanelRequestedEvent _)
+ {
+ if (this == null) return;
+ OpenChatPanel();
+ }
+
private bool ShouldShowMessage(ChatMessageData data)
{
if (_currentFilterChannel == ChatChannel.GP_CHAT_LOCAL) return true;
@@ -226,8 +228,6 @@ namespace BrewMonster.Scripts.ChatUI
if (_messages.Count > maxStoredMessages)
_messages.RemoveAt(0);
- RefreshMiniChat();
-
if (chatPanelUIGO == null || !chatPanelUIGO.activeSelf)
return;
@@ -309,68 +309,6 @@ namespace BrewMonster.Scripts.ChatUI
ScrollToBottom();
}
- void RefreshMiniChat()
- {
- if (miniChatContent == null) return;
-
- _miniFilterBuffer.Clear();
- foreach (var msg in _messages)
- {
- if (ShouldShowMessage(msg))
- _miniFilterBuffer.Add(msg);
- }
-
- int take = Mathf.Min(Mathf.Max(1, miniChatPreviewLines), _miniFilterBuffer.Count);
- int startIdx = _miniFilterBuffer.Count - take;
-
- while (_miniChatViews.Count < take)
- {
- var v = CreateMiniChatItem();
- if (v == null) return;
- _miniChatViews.Add(v);
- }
-
- while (_miniChatViews.Count > take)
- {
- var last = _miniChatViews[_miniChatViews.Count - 1];
- _miniChatViews.RemoveAt(_miniChatViews.Count - 1);
- if (last != null)
- Destroy(last.gameObject);
- }
-
- for (int i = 0; i < take; i++)
- {
- var data = _miniFilterBuffer[startIdx + i];
- var view = _miniChatViews[i];
- Sprite icon = _iconCache.ContainsKey(data.channel) ? _iconCache[data.channel] : null;
- view.gameObject.SetActive(true);
- view.transform.SetSiblingIndex(i);
- if (miniChatPassThroughClicks)
- DisableGraphicsRaycastUnder(view.transform);
- view.Bind(icon, data.message);
- }
-
- Canvas.ForceUpdateCanvases();
- }
-
- ///
- /// Mini chat rows use TMP/Image with raycastTarget on — they sit above the open button and steal clicks.
- ///
- static void DisableGraphicsRaycastUnder(Transform root)
- {
- if (root == null) return;
- foreach (var g in root.GetComponentsInChildren(true))
- g.raycastTarget = false;
- }
-
- ChatMessageView CreateMiniChatItem()
- {
- var prefab = miniMessagePrefab != null ? miniMessagePrefab : messagePrefab;
- if (prefab == null || miniChatContent == null) return null;
- var item = Instantiate(prefab, miniChatContent, false);
- return item;
- }
-
public void ScrollToBottom()
{
Canvas.ForceUpdateCanvases();
@@ -387,24 +325,13 @@ namespace BrewMonster.Scripts.ChatUI
_visibleViews.Clear();
_messages.Clear();
- ClearMiniChatViews();
- }
-
- void ClearMiniChatViews()
- {
- foreach (var v in _miniChatViews)
- {
- if (v != null)
- Destroy(v.gameObject);
- }
-
- _miniChatViews.Clear();
}
public void OnHandlerChatButton()
{
+ if (chatPanelUIGO == null) return;
bool open = !chatPanelUIGO.activeSelf;
- chatPanelUIGO.SetActive(open);
+ SetChatPanelAndBgVisible(open);
if (open)
RefreshVisible();
@@ -415,7 +342,7 @@ namespace BrewMonster.Scripts.ChatUI
public void OpenChatPanel()
{
if (chatPanelUIGO == null) return;
- chatPanelUIGO.SetActive(true);
+ SetChatPanelAndBgVisible(true);
RefreshVisible();
_chatInput ??= GetComponent();
@@ -425,8 +352,7 @@ namespace BrewMonster.Scripts.ChatUI
public void CloseChatPanel()
{
- if (chatPanelUIGO == null) return;
- chatPanelUIGO.SetActive(false);
+ SetChatPanelAndBgVisible(false);
SetEmojiPanelVisible(false);
}
@@ -454,5 +380,10 @@ namespace BrewMonster.Scripts.ChatUI
}
}
- public struct OnEventClearChat{}
+ public struct OnEventClearChat { }
+
+ ///
+ /// Mini chat (hoặc HUD) publish để mở panel chat đầy đủ; subscribe.
+ ///
+ public struct OpenChatPanelRequestedEvent { }
}
diff --git a/Assets/PerfectWorld/Scripts/Chat/UI/EmojiButtonCell.cs b/Assets/PerfectWorld/Scripts/Chat/UI/EmojiButtonCell.cs
new file mode 100644
index 0000000000..be3fcf7f35
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Chat/UI/EmojiButtonCell.cs
@@ -0,0 +1,70 @@
+using System;
+using TMPro;
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace BrewMonster.Scripts.ChatUI
+{
+ ///
+ /// Một ô emoji trong bảng chọn emoji — One emoji cell in the emoji picker grid.
+ /// Gán lên prefab Button có Image + (tuỳ chọn) TMP_Text cho tooltip.
+ ///
+ [RequireComponent(typeof(Button))]
+ public class EmojiButtonCell : MonoBehaviour
+ {
+ [SerializeField] Image _icon;
+ [Tooltip("Tuỳ chọn: hiện hint text (tên emoji). Optional: show hint/tooltip text.")]
+
+ public event Action OnClicked; // (emotionSet, emotionIndex)
+
+ private int _emotionSet;
+ private int _emotionIndex;
+ private Button _button;
+
+ private void Awake()
+ {
+ _button = GetComponent