From 9e81951f1250c531d1c010c00b22892b766ff2ba Mon Sep 17 00:00:00 2001 From: CuongNV <> Date: Wed, 1 Apr 2026 10:41:22 +0700 Subject: [PATCH 1/6] remove log and change ErrorChatHandler --- .../Scripts/Chat/UI/ServerErrorChatHandler.cs | 2 +- .../Scripts/Network/CSNetwork/GameSession.cs | 10 +--------- .../Scripts/UI/GamePlay/EC_GameUIMan.cs | 13 ++++++------- Assets/Scripts/ChatInputHandler.cs | 2 -- Assets/Scripts/EC_GameRun.cs | 4 ++-- Assets/Scripts/EC_GameRunChat.cs | 10 ++++------ 6 files changed, 14 insertions(+), 27 deletions(-) diff --git a/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs b/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs index 9dc5e3ff99..f3116fe94a 100644 --- a/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs +++ b/Assets/PerfectWorld/Scripts/Chat/UI/ServerErrorChatHandler.cs @@ -31,7 +31,7 @@ namespace BrewMonster.Scripts.ChatUI errorMsg = $"Lỗi không xác định"; } - string coloredMsg = $"[System Error {e.ErrorCode}] {errorMsg}"; + string coloredMsg = $"{errorMsg}"; EventBus.Publish(new GameSession.ChatMessageEvent( coloredMsg, diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs index 1c91a90cf4..6fe9391a70 100644 --- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs @@ -2094,11 +2094,9 @@ namespace CSNetwork chatmessage p = (chatmessage)pProtocol; //var channel = (ChatChannel)p.Channel; - Debug.Log("[Cuong] reciver chat channel: " + p.Channel); if (Chat_GameSession.ShouldBlockByLevel(p)) { - Debug.Log("[Cuong] 1"); return true; } @@ -2111,7 +2109,6 @@ namespace CSNetwork strTemp = AUICommon.FilterInvalidTags(strTemp, pItem == null); if (!Chat_GameSession.PolicyResolver(pProtocol, p, ref strTemp, out szMsg)) { - Debug.Log("[Cuong] 2"); return false; } @@ -2160,18 +2157,15 @@ namespace CSNetwork } else { - Debug.Log("[Cuong] 5"); EventBus.Publish(new ChatMessageEvent(strTemp, p.Channel)); } }else if (p.Channel == (byte)ChatChannel.GP_CHAT_INSTANCE && p.Srcroleid == 1) { - Debug.Log("[Cuong] 6"); // Chat_GameSession.AUICTranslate trans; // EC_Game.GetGameRun().GetUIManager().GetInGameUIMan().AddHeartBeatHint(trans.Translate(szMsg )); } else { - Debug.Log("[Cuong] Other"); CECStringTab pStrTab = EC_Game.GetFixedMsgs(); if (ISPLAYERID(p.Srcroleid)) @@ -2179,7 +2173,6 @@ namespace CSNetwork string szName = EC_Game.GetGameRun().GetPlayerName(p.Srcroleid, false); if (string.IsNullOrEmpty(szName)) { - Debug.Log("[Cuong] Other 0"); if (!bCalledagain) { Chat_GameSession.AddElemForPendingProtocols(pProtocol); @@ -2189,7 +2182,6 @@ namespace CSNetwork } else { - Debug.Log("[Cuong] Other 1"); char[] szText = new char[80]; AUICommon.AUI_ConvertChatString(ref szName,ref szText, false); @@ -2210,7 +2202,7 @@ namespace CSNetwork } else if(ISNPCID(p.Srcroleid)) { - Debug.Log("[Cuong] ISNPCID " + strTemp); + BMLogger.Log("[Cuong] ISNPCID " + strTemp); CECNPC pNPC = EC_Game.GetGameRun().GetWorld().GetNPCMan().GetNPC(p.Srcroleid); if (pNPC != null) diff --git a/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs b/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs index b538a03e37..04891faf0d 100644 --- a/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs +++ b/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs @@ -534,7 +534,6 @@ namespace BrewMonster.UI // C++: Write to m_pDlgChatWhisper, Chat1, Chat2, SuperFarCry // Unity equivalent: Dispatch to EventBus for ChatPanelUI to handle - Debug.Log($"[Cuong][{cChannel}] {formattedMsg}"); EventBus.Publish(new GameSession.ChatMessageEvent(formattedMsg, (byte)cChannel)); // C++: AddChatMessage also handles head bubble via pPlayer->SetLastSaidWords @@ -635,7 +634,7 @@ namespace BrewMonster.UI // m_vecIconList.push_back( m_pA2DSpriteIcons[h] ); // add for imaged hints // } - + // SetImageList(&m_vecIconList); // add for imaged hints // for( h = 0; h < ICONS_MAX; h++ ) @@ -643,7 +642,7 @@ namespace BrewMonster.UI // bval = m_pA2DSpriteIcons[h]->ResetItems( // a_nCountX[h] * a_nCountY[h], a_rc[h]); // a_free(a_rc[h]); - // if( !bval ) return AUI_ReportError(__LINE__, __FILE__); + // if( !bval ) return AUI_ReportError(__LINE__, __FILE__); // } // m_pA2DSpriteMask = new A2DSprite; @@ -658,7 +657,7 @@ namespace BrewMonster.UI // if( !m_pA2DSpriteItemExpire ) return AUI_ReportError(__LINE__, __FILE__); // bval = m_pA2DSpriteItemExpire->Init(m_pA3DDevice, "InGame\\IconItemExpire.dds", AUI_COLORKEY); // if( !bval ) - // { + // { // delete m_pA2DSpriteItemExpire; // m_pA2DSpriteItemExpire = NULL; // AUI_ReportError(__LINE__, "CECGameUIMan::LoadIconSet(), failed to load InGame\\IconItemExpire.dds"); @@ -720,7 +719,7 @@ namespace BrewMonster.UI // // ����ռλ�� // m_pA2DSpriteImage.push_back(NULL); // } - + // PAUIDIALOG pShow = GetDialog("Win_Popface"); // pShow->SetData(AUIMANAGER_MAX_EMOTIONGROUPS); // pShow = GetDialog("Win_Popface01"); @@ -760,10 +759,10 @@ namespace BrewMonster.UI // m_pSpriteIconSysModule.push_back(pSprite); // } - + // return true; // } - + } public enum EC_GAMEUI_ICONS : byte diff --git a/Assets/Scripts/ChatInputHandler.cs b/Assets/Scripts/ChatInputHandler.cs index 8eec819abf..3e1750c16a 100644 --- a/Assets/Scripts/ChatInputHandler.cs +++ b/Assets/Scripts/ChatInputHandler.cs @@ -503,8 +503,6 @@ namespace BrewMonster.Scripts.ChatUI // 5. Publish event EventBus.Publish(new GameSession.ChatMessageEvent(finalMsg, (byte)channel)); - - Debug.Log("[Cuong] AddChatMessage" + finalMsg); } private string GetChannelColorHex(ChatChannel channel) diff --git a/Assets/Scripts/EC_GameRun.cs b/Assets/Scripts/EC_GameRun.cs index f4fb44f9cb..0932be7aba 100644 --- a/Assets/Scripts/EC_GameRun.cs +++ b/Assets/Scripts/EC_GameRun.cs @@ -796,7 +796,7 @@ public partial class CECGameRun : ITickable CECGameUIMan pGameUI = m_pUIManager?.GetInGameUIMan(); if (pGameUI != null) { - //pGameUI.AddChatMessage(szFormattedMsg, (int)GP_CHAT.GP_CHAT_SYSTEM); + pGameUI.AddChatMessage(szFormattedMsg, ChatChannel.GP_CHAT_SYSTEM); } else { @@ -888,7 +888,7 @@ public partial class CECGameRun : ITickable else { // Fallback to debug output if UI is not available - Debug.LogError($"[Cuong] [Error] pGameUI == null"); + BMLogger.LogError($"[Cuong] [Error] pGameUI == null"); // Note: In C++ original, pItem is deleted here if UI is not available // In C#, we don't need explicit deletion due to garbage collection diff --git a/Assets/Scripts/EC_GameRunChat.cs b/Assets/Scripts/EC_GameRunChat.cs index 5ba9947eaf..5f480a64b1 100644 --- a/Assets/Scripts/EC_GameRunChat.cs +++ b/Assets/Scripts/EC_GameRunChat.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using BrewMonster; using CSNetwork; using CSNetwork.GPDataType; using BrewMonster.Common; @@ -42,8 +43,6 @@ partial class CECGameRun /// public void ShowAccountLoginInfo() { - Debug.Log("[Cuong] ShowAccountLoginInfo"); - // Kiểm tra cờ xem đã hiển thị thông tin đăng nhập chưa để tránh in lặp lại nhiều lần. if (!m_bAccountLoginInfoShown) { @@ -68,7 +67,7 @@ partial class CECGameRun string textTime = AUIDialog.FormatPrintf(pGameUI.GetStringFromTable(9343), timeStr); EventBus.Publish(new GameSession.ChatMessageEvent { context = textTime, channel = (byte)ChatChannel.GP_CHAT_SYSTEM }); - Debug.Log($"[Cuong] ShowAccountLoginInfo {textTime}"); + BMLogger.Log($"[Cuong] ShowAccountLoginInfo {textTime}"); // 2. Định dạng và hiển thị IP đăng nhập lần trước (Last login IP). // Table 9344: chuỗi dạng "IP đăng nhập trước: %s" @@ -76,7 +75,7 @@ partial class CECGameRun string textIp = AUIDialog.FormatPrintf(pGameUI.GetStringFromTable(9344), ipStr); EventBus.Publish(new GameSession.ChatMessageEvent { context = textIp, channel = (byte)ChatChannel.GP_CHAT_SYSTEM }); - Debug.Log($"[Cuong] ShowAccountLoginInfo {textIp}"); + BMLogger.Log($"[Cuong] ShowAccountLoginInfo {textIp}"); // 3. Định dạng và hiển thị IP đăng nhập hiện tại (Current login IP). // Table 9345: chuỗi dạng "IP đăng nhập hiện tại: %s" @@ -84,7 +83,7 @@ partial class CECGameRun string textCurIp = AUIDialog.FormatPrintf(pGameUI.GetStringFromTable(9345), curIpStr); EventBus.Publish(new GameSession.ChatMessageEvent { context = textCurIp, channel = (byte)ChatChannel.GP_CHAT_SYSTEM }); - Debug.Log($"[Cuong] ShowAccountLoginInfo {textCurIp}"); + BMLogger.Log($"[Cuong] ShowAccountLoginInfo {textCurIp}"); } } } @@ -106,7 +105,6 @@ partial class CECGameRun { string text = "Hoàn tất thông tin tài khoản..."; // 9347 EventBus.Publish(new GameSession.ChatMessageEvent { context = text, channel = (byte)ChatChannel.GP_CHAT_SYSTEM }); - Debug.Log($"[Cuong] ShowAccountInfo {text}"); } } } From a82337f61787cddb7dfafba972e5841da446ed27 Mon Sep 17 00:00:00 2001 From: CuongNV <> Date: Wed, 1 Apr 2026 16:47:02 +0700 Subject: [PATCH 2/6] update skill for chat --- .agents/skills/chat_system_csharp/SKILL.md | 26 ++++++ .../CONVERSION_GUIDE_STRING_FORMAT.md | 83 +++++++++++++++++-- 2 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 .agents/skills/chat_system_csharp/SKILL.md diff --git a/.agents/skills/chat_system_csharp/SKILL.md b/.agents/skills/chat_system_csharp/SKILL.md new file mode 100644 index 0000000000..d8725dfd91 --- /dev/null +++ b/.agents/skills/chat_system_csharp/SKILL.md @@ -0,0 +1,26 @@ +--- +name: C# Chat System Formatting +description: Hướng dẫn viết code C# cho hệ thống chat dùng AUIDialog.FormatPrintf và pGameUI.GetStringFromTable. +--- + +# C# Chat System Formatting + +Skill này cung cấp hướng dẫn khi viết code C# cho hệ thống chat (ví dụ như khi chuyển đổi code từ C++ sang C# trong Unity). + +## Quy tắc lấy chuỗi và format + +Khi cần lấy một chuỗi từ bảng ngôn ngữ (Table) và format chuỗi đó với các tham số (thay thế cho %s, %d,...), hãy sử dụng kết hợp `pGameUI.GetStringFromTable` và `AUIDialog.FormatPrintf`. + +Trong phiên bản C++ gốc (ví dụ trong `EC_GameUIMan.cpp`), việc lấy chuỗi thường dùng `GetStringFromTable` và format bằng `swprintf` hoặc các hàm tương tự. Ở phiên bản C#, chúng ta thực hiện như sau: + +```csharp +// Ví dụ Table 9343: chuỗi dạng "Thời gian đăng nhập trước: %s" +// AUIDialog.FormatPrintf sẽ thực hiện kết hợp giữa pGameUI.GetStringFromTable(9343) và chuỗi timeStr +string textTime = AUIDialog.FormatPrintf(pGameUI.GetStringFromTable(9343), timeStr); +``` + +## Các bước thực hiện +1. Đảm bảo đã lấy được referent tới `pGameUI`, ví dụ qua `CECUIManager.Instance?.GetInGameUIMan()`. +2. Lấy chuỗi format gốc từ bảng ngôn ngữ: `pGameUI.GetStringFromTable(ID)`. +3. Dùng `AUIDialog.FormatPrintf` để thay thế các placeholder (như `%s`, `%d`) trong chuỗi lấy được bằng các giá trị thực tế. +4. Gửi chuỗi đã được format đến hệ thống chat, ví dụ qua `EventBus.Publish(new GameSession.ChatMessageEvent {...})`. diff --git a/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md b/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md index 7435b3639a..6db6b8bb73 100644 --- a/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md +++ b/Documentation/CONVERSION_GUIDE_STRING_FORMAT.md @@ -66,16 +66,23 @@ ACString str; str.Format(GetStringFromTable(11326), needMoney, needSp); ``` -**C# (string)**: +**C#**: +**Method A: Using `AUIDialog.FormatPrintf` (Recommended for Legacy Strings)** +If the string from the table uses C++ format specifiers (`%d`, `%s`, `%f`), `AUIDialog.FormatPrintf` handles them natively without requiring string conversion. +```csharp +string confirmMessage = AUIDialog.FormatPrintf(GetStringFromTable(11326), needMoney, needSp); +``` + +**Method B: Using `string.Format` (Standard C#)** +If the string table has been updated to use C# format specifiers (`{0}`, `{1}`), you can use the built-in `string.Format`. ```csharp string confirmMessage = string.Format(GetStringFromTable(11326), needMoney, needSp); ``` **Notes**: -- C++'s `ACString::Format()` is a member function -- C#'s `string.Format()` is a static function -- Both use the same placeholder syntax: `%d` (C++) or `{0}`, `{1}` (C#) -- If your string table uses C++ format specifiers, you may need to convert them: +- C++'s `ACString::Format()` is a member function. +- C#'s `string.Format()` is a static function but only understands `{0}`, `{1}` syntax. +- If your string table still uses C++ format specifiers (`%d`, `%s`, `%f`), **you should use `AUIDialog.FormatPrintf`**. Otherwise, you would need to manually convert the placeholders in the string table: - `%d` → `{0}`, `{1}` for integers - `%s` → `{0}`, `{1}` for strings - `%f` → `{0}`, `{1}` for floats @@ -346,11 +353,66 @@ if (spOK) { } ``` +### Example 4: Clickable Text Links (TextMeshPro) + +**C++ (Rich Text and Actions)**: +In C++, player names or interactive elements (like items) in chat are often wrapped with custom delimiters like `&PlayerName&` to be parsed later for coloring and clicking. The click actions are handled by complex UI dialogue components. + +**C# (Unity TextMeshPro `` tag)**: +In Unity's TextMeshPro, we can use the standard `` tag to define an interactive region of text. By parsing the legacy `&PlayerName&` format using Regex, we can inject a `` tag that Unity's event system can interact with. + +1. **Format the string with Regex replacing delimiters (e.g., in `EC_GameUIMan.cs`)**: +```csharp +if (parsedMsg.Contains("&")) +{ + // Convert &PlayerName& into PlayerName + parsedMsg = System.Text.RegularExpressions.Regex.Replace( + parsedMsg, + @"&([^&]+)&", + $"$1" + ); +} +``` + +2. **Handle the Click Event via Unity EventSystems (e.g., in `ChatMessageView.cs`)**: +The UI View script attached to the TextMeshPro text must implement `IPointerClickHandler` to intercept pointer clicks on the `` markup. +```csharp +using UnityEngine.EventSystems; +using TMPro; + +public class ChatMessageView : MonoBehaviour, IPointerClickHandler +{ + public EC_UIUtility.TextOutlet messageText; + + public void OnPointerClick(PointerEventData eventData) + { + if (messageText == null || messageText.tmp == null) return; + + // Check if the pointer click intersects with any tag boundary + int linkIndex = TMP_TextUtilities.FindIntersectingLink(messageText.tmp, eventData.position, eventData.pressEventCamera); + if (linkIndex != -1) + { + // Retrieve the link ID (which we dynamically set to the player's name) + TMP_LinkInfo linkInfo = messageText.tmp.textInfo.linkInfo[linkIndex]; + string linkId = linkInfo.GetLinkID(); + + if (!string.IsNullOrEmpty(linkId)) + { + // Trigger logic, e.g., Whisper the player! + EventBus.Publish(new WhisperPlayerEvent(linkId)); + } + } + } +} +``` + +**Note for specific PW Dialogues**: Legacy classes (such as `DlgNameLink.cs`) may implement a customized command pattern (e.g., `LinkCommand`, `MoveToLinkCommand` alongside `StyledTaskTraceText`) to process complex hyperlink commands recursively. However, for completely rewritten or standalone UI systems like standard Chat or logging panels, utilizing TextMeshPro's native `TMP_TextUtilities.FindIntersectingLink` combined with `IPointerClickHandler` is significantly faster, lightweight, and standard for Unity development. + --- ## Common Pitfalls -### ❌ Wrong: Using C++ format specifiers in C# +### ❌ Wrong: Using C++ format specifiers with `string.Format` ```csharp // This won't work if the string uses C++ format specifiers @@ -358,8 +420,15 @@ string template = "Cost: %d gold"; // C++ style string message = string.Format(template, 1000); // Error! ``` -### ✅ Correct: Convert to C# format +### ✅ Correct: Use `AUIDialog.FormatPrintf` OR convert to C# format +**Option 1: Use AUIDialog.FormatPrintf for C++ styles:** +```csharp +string template = "Cost: %d gold"; // C++ style +string message = AUIDialog.FormatPrintf(template, 1000); // "Cost: 1000 gold" +``` + +**Option 2: Convert to C# format strings:** ```csharp string template = "Cost: {0} gold"; // C# style string message = string.Format(template, 1000); // "Cost: 1000 gold" From 56923e1b404c9150d1b974a92c2982198e62ef97 Mon Sep 17 00:00:00 2001 From: CuongNV <> Date: Wed, 1 Apr 2026 16:50:38 +0700 Subject: [PATCH 3/6] update logic chatsystem --- .../Scripts/Chat/UI/ChatMessageView.cs | 41 ++++++++++++++++++- .../Scripts/Network/CSNetwork/GameSession.cs | 4 +- .../Scripts/UI/Dialogs/DlgPlayerOptions.cs | 6 +-- .../Scripts/UI/GamePlay/EC_GameUIMan.cs | 4 +- Assets/Scripts/ChatInputHandler.cs | 2 +- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs b/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs index c83e5943df..b49d91a3ec 100644 --- a/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs +++ b/Assets/PerfectWorld/Scripts/Chat/UI/ChatMessageView.cs @@ -3,10 +3,13 @@ using BrewMonster.Scripts.UI; using TMPro; using UnityEngine; using UnityEngine.UI; +using UnityEngine.EventSystems; +using BrewMonster.Network; +using CSNetwork.GPDataType; namespace BrewMonster.Scripts.ChatUI { - public class ChatMessageView : MonoBehaviour + public class ChatMessageView : MonoBehaviour, IPointerClickHandler { public Image iconImage; public EC_UIUtility.TextOutlet messageText; @@ -24,5 +27,41 @@ namespace BrewMonster.Scripts.ChatUI messageText.Set(message); GetComponent().RefreshLayout(); } + + public void OnPointerClick(PointerEventData eventData) + { + if (messageText == null || messageText.tmp == null) return; + + int linkIndex = TMP_TextUtilities.FindIntersectingLink(messageText.tmp, eventData.position, eventData.pressEventCamera); + if (linkIndex != -1) + { + TMP_LinkInfo linkInfo = messageText.tmp.textInfo.linkInfo[linkIndex]; + string linkId = linkInfo.GetLinkID(); + if (!string.IsNullOrEmpty(linkId)) + { + string playerName = linkId; + int characterId = 0; + + if (linkId.Contains("|")) + { + string[] parts = linkId.Split('|'); + if (parts.Length == 2) + { + int.TryParse(parts[0], out characterId); + playerName = parts[1]; + } + } + + var host = EC_Game.GetGameRun()?.GetHostPlayer(); + if (host != null && characterId > 0 && characterId != host.GetCharacterID() && GPDataTypeHelper.ISPLAYERID(characterId)) + { + CECUIManager.Instance?.ShowPlayerOptionsDialog(characterId, eventData.position + new Vector2(0, 400)); + } + + Debug.Log("[Cuong] OnWhisper from Chat: name: " + playerName); + EventBus.Publish(new WhisperPlayerEvent(playerName)); + } + } + } } } diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs index 6fe9391a70..4433c48713 100644 --- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs @@ -2132,7 +2132,7 @@ namespace CSNetwork break; case 18: case 19: case 20: case 21: case 22: // Auction Message // pGameUI.AddSysAuctionMessage(...) - Debug.Log("[Auction] " + strTemp); + Debug.Log("[Cuong] Auction " + strTemp); EventBus.Publish(new ChatMessageEvent(strTemp, p.Channel)); break; case 24: // Task Message @@ -2252,7 +2252,7 @@ namespace CSNetwork { // Format: "[Name] whispers to [You]: [Message]" string fmt = AUICommon.ConvertPrintfToCSharpFormat(pStrTab.GetWideString((int)FixedMsg.FIXMSG_PRIVATECHAT1)); - + BMLogger.Log($"[Cuong] OnPrtcPrivateChat {fmt}"); string formatted; try { formatted = string.Format(fmt, strSrcName, strMsg); diff --git a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs index 1254ab6dc8..9240ef47eb 100644 --- a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs +++ b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgPlayerOptions.cs @@ -91,9 +91,9 @@ namespace BrewMonster.UI void OnWhisper(int characterId) { string name = EC_ManMessageMono.Instance?.GetECManPlayer?.GetElsePlayer(characterId)?.GetName() ?? ""; - Debug.Log("OnWhisper: " + characterId + " name: " + name); - - EventBus.Publish(new BrewMonster.Scripts.ChatUI.WhisperPlayerEvent(name)); + Debug.Log("[Cuong] OnWhisper: " + characterId + " name: " + name); + + EventBus.Publish(new Scripts.ChatUI.WhisperPlayerEvent(name)); } void PositionAtMouse() diff --git a/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs b/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs index 04891faf0d..d11b434cc6 100644 --- a/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs +++ b/Assets/PerfectWorld/Scripts/UI/GamePlay/EC_GameUIMan.cs @@ -516,11 +516,11 @@ namespace BrewMonster.UI string parsedMsg = pszMsg; if (parsedMsg.Contains("&")) { - // Convert &Cuong& into Cuong + // Convert &Cuong& into Cuong parsedMsg = System.Text.RegularExpressions.Regex.Replace( parsedMsg, @"&([^&]+)&", - $"$1" + $"$1" ); } else diff --git a/Assets/Scripts/ChatInputHandler.cs b/Assets/Scripts/ChatInputHandler.cs index 3e1750c16a..c44b26a58d 100644 --- a/Assets/Scripts/ChatInputHandler.cs +++ b/Assets/Scripts/ChatInputHandler.cs @@ -429,7 +429,7 @@ namespace BrewMonster.Scripts.ChatUI private void SendPrivateChat(string target, string msg, int pack, int slot) { - Debug.Log($"Whisper to {target}: {msg}"); + BMLogger.Log($"[Cuong] Whisper to {target}: {msg}"); // [Port] C++: CDlgChat::OnCommand_speak → privatechat branch // Gửi tin nhắn mật (whisper) lên server qua GameSession. From aad2789f58df554f2ef933e06f7d02a8ee0ab3ca Mon Sep 17 00:00:00 2001 From: CuongNV <> Date: Thu, 2 Apr 2026 11:42:34 +0700 Subject: [PATCH 4/6] add logic whisper --- .../PerfectWorld/Scripts/UI/Chat/DlgChat.cs | 22 +++++- Assets/Scripts/ChatInputHandler.cs | 73 ++++++++++++++++--- 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs b/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs index 236e8b1530..ed432b3e49 100644 --- a/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs +++ b/Assets/PerfectWorld/Scripts/UI/Chat/DlgChat.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using BrewMonster.Network; using CSNetwork.GPDataType; namespace BrewMonster.UI @@ -29,8 +30,11 @@ namespace BrewMonster.UI { ChatChannel.GP_CHAT_COUNTRY, "FFFFFF" } }; - public const string NPC_COLOR = "C8FF64"; - public const string KING_COLOR = "8A2BE2"; + public const string NPC_COLOR = "C8FF64"; + public const string KING_COLOR = "8A2BE2"; + // [Port] C++: CDlgChat::m_pszWhisperFriendColor = "^FF4AB0" + // Màu hồng cho whisper từ/tới bạn bè trong danh sách + public const string WHISPER_FRIEND_COLOR = "FF4AB0"; /// /// Formats a message with TextMeshPro color tags based on the channel. @@ -55,10 +59,22 @@ namespace BrewMonster.UI /// /// Returns the hex color string for a given channel. /// Mirrors CDlgChat::GetChatColor in C++. - /// idPlayer is reserved for future per-player coloring (e.g. friend highlight). /// + /// Chat channel + /// Sender/recipient role ID — used to check friend status for GP_CHAT_WHISPER public static string GetChatColor(ChatChannel channel, int idPlayer = -1) { + // [Port] C++: DlgChat.cpp dòng 142-157 + // if (iChannel == GP_CHAT_WHISPER && pFriendMan->GetFriendByID(idPlayer)) + // return m_pszWhisperFriendColor; // "^FF4AB0" (hồng) + if (channel == ChatChannel.GP_CHAT_WHISPER && idPlayer > 0) + { + var host = EC_Game.GetGameRun()?.GetHostPlayer(); + var friendMan = host?.GetFriendMan(); + if (friendMan != null && friendMan.GetFriendByID(idPlayer).HasValue) + return WHISPER_FRIEND_COLOR; + } + if (ChannelColors.TryGetValue(channel, out string hexColor)) return hexColor; return "FFFFFF"; diff --git a/Assets/Scripts/ChatInputHandler.cs b/Assets/Scripts/ChatInputHandler.cs index c44b26a58d..9204c57da9 100644 --- a/Assets/Scripts/ChatInputHandler.cs +++ b/Assets/Scripts/ChatInputHandler.cs @@ -4,6 +4,7 @@ using TMPro; using CSNetwork.GPDataType; using System.Collections.Generic; using BrewMonster.Network; +using BrewMonster.UI; using CSNetwork; namespace BrewMonster.Scripts.ChatUI @@ -433,11 +434,54 @@ namespace BrewMonster.Scripts.ChatUI // [Port] C++: CDlgChat::OnCommand_speak → privatechat branch // Gửi tin nhắn mật (whisper) lên server qua GameSession. - UnityGameSession.Instance.GameSession.SendPrivateChatData(target, msg, 0, 0, pack, slot); + // Tra cứu ID của người nhận từ danh sách bạn bè (giống C++: pFriend ? pFriend->id : -1) + int idTarget = GetCharacterIdByName(target); + UnityGameSession.Instance.GameSession.SendPrivateChatData(target, msg, 0, idTarget, pack, slot); - // Server không echo whisper lại cho chính mình → hiện local echo - string localEcho = $"{target}: {msg}"; - EventBus.Publish(new GameSession.ChatMessageEvent(localEcho, (byte)ChatChannel.GP_CHAT_WHISPER)); + // [Port] C++: DlgChat.cpp dòng 1531-1532 + // Server KHÔNG echo whisper lại cho người gửi → phải hiển thị local. + // C++ dùng: a_sprintf(szMsg, GetStringFromTable(233), szName, szText) + // → format "对&Name&说:msg" — dấu & được thêm bởi FORMAT STRING, không phải bởi caller. + // C# dùng FIXMSG_PRIVATECHAT2 tương ứng; fallback nếu format string lỗi. + // QUAN TRỌNG: chỉ truyền target (không có & bao quanh) vào string.Format. + // Nếu format string đã có &{0}& thì kết quả đúng là "&target&". + // Nếu format string không có & thì AddChatMessage cũng chỉ wrap màu bình thường. + // Tránh truyền "&target&" vì kết quả sẽ là "&&target&&" → regex chỉ match "target" + // nhưng để lại & dư ở ngoài → UI hiển thị "&target&" thay vì link sạch. + CECStringTab pStrTab = EC_Game.GetFixedMsgs(); + string fmt = AUICommon.ConvertPrintfToCSharpFormat(pStrTab.GetWideString((int)FixedMsg.FIXMSG_PRIVATECHAT2)); + string localMsg; + try + { + localMsg = string.Format(fmt, target, msg); + } + catch + { + // Fallback: dùng &target& để AddChatMessage tạo link — format không có & thì mình thêm + localMsg = $"&{target}&: {msg}"; + } + + CECGameUIMan pGameUI = EC_Game.GetGameRun()?.GetUIManager()?.GetInGameUIMan(); + if (pGameUI != null) + { + // idTarget là ID người NHẬN (hoặc -1 nếu không tìm thấy trong friend list) + // AddChatMessage sẽ convert &target& thành TMP link tag có dạng idTarget|target + pGameUI.AddChatMessage(localMsg, ChatChannel.GP_CHAT_WHISPER, idTarget, null, 0, 0, null, msg); + } + } + + // [Port] C++: pFriendMan->GetFriendByName(name) → pFriend->id + // Tìm character ID của người chơi theo tên từ friend list. + // Trả về -1 nếu không tìm thấy (giống C++ trả về idFriend = -1). + private int GetCharacterIdByName(string name) + { + if (string.IsNullOrEmpty(name)) return -1; + var host = EC_Game.GetGameRun()?.GetHostPlayer(); + if (host == null) return -1; + var friendMan = host.GetFriendMan(); + if (friendMan == null) return -1; + var friend = friendMan.GetFriendByName(name); + return friend.HasValue ? friend.Value.Id : -1; } // ===================================================== @@ -507,14 +551,23 @@ namespace BrewMonster.Scripts.ChatUI private string GetChannelColorHex(ChatChannel channel) { - // Simplified mapping based on common PW colors + // [Port] C++: CDlgChat::m_pszColor[] — DlgChat.cpp dòng 120-136 + // Giữ đúng màu gốc cho các kênh dùng trong local error messages return channel switch { - ChatChannel.GP_CHAT_TEAM => "00FFFF", // Cyan - ChatChannel.GP_CHAT_FACTION => "00FF00", // Green - ChatChannel.GP_CHAT_FARCRY => "FFFF00", // Yellow - ChatChannel.GP_CHAT_WHISPER => "FF00FF", // Magenta - _ => "FFFFFF" // White + ChatChannel.GP_CHAT_LOCAL => "FFFFFF", + ChatChannel.GP_CHAT_FARCRY => "FFE400", + ChatChannel.GP_CHAT_TEAM => "00FF00", + ChatChannel.GP_CHAT_FACTION => "00FFFC", + ChatChannel.GP_CHAT_WHISPER => "0065FE", // C++: "^0065FE" — KHÔNG phải FF00FF + ChatChannel.GP_CHAT_TRADE => "FF742E", + ChatChannel.GP_CHAT_SYSTEM => "BED293", + ChatChannel.GP_CHAT_BROADCAST => "FF3600", + ChatChannel.GP_CHAT_MISC => "9AA6FF", + ChatChannel.GP_CHAT_SUPERFARCRY => "ff9b3e", + ChatChannel.GP_CHAT_BATTLE => "FFFFFF", + ChatChannel.GP_CHAT_COUNTRY => "FFFFFF", + _ => "FFFFFF" }; } From 569d987dcf16e30e265ff0775af187d86327c159 Mon Sep 17 00:00:00 2001 From: HungDK <> Date: Thu, 2 Apr 2026 17:32:51 +0700 Subject: [PATCH 5/6] Add buying stackable item, not stackable. Fixing cant buy item from 2nd tabs --- .../Scripts/UI/NPCShopUIManager.cs | 278 +++++++++++++----- Assets/Prefabs/UI/DialogNPCShop.prefab | 6 +- 2 files changed, 209 insertions(+), 75 deletions(-) diff --git a/Assets/PerfectWorld/Scripts/UI/NPCShopUIManager.cs b/Assets/PerfectWorld/Scripts/UI/NPCShopUIManager.cs index 37e9c69ec1..94820c8658 100644 --- a/Assets/PerfectWorld/Scripts/UI/NPCShopUIManager.cs +++ b/Assets/PerfectWorld/Scripts/UI/NPCShopUIManager.cs @@ -29,9 +29,9 @@ public class NPCShopUIManager : AUIDialog [Header("Texts")] public TextMeshProUGUI itemDetailNameText; - public TextMeshProUGUI itemDetailPriceText; - public TextMeshProUGUI itemsPriceText; public TextOutlet itemDescriptionText; + public TextMeshProUGUI itemsBuyAmountText; + public TextMeshProUGUI itemsBuyTotalMoneyText; [Header("Tabs")] public Transform tabButtonContainer; @@ -74,6 +74,10 @@ public class NPCShopUIManager : AUIDialog private int shopItemIndex; private Color colorUnActive = new Color(1f, 1f, 1f, 0f); private Color colorActive = new Color(1f, 1f, 1f, 1f); + private int buyCount = 1; + private const int BuyCountMin = 1; + private const string BuyUiEmptyValue = "0"; + private const int BuyCountFallbackMax = 99; /// Current NPC id for this shop session. Send SEVNPC_HELLO with this before buy. public uint CurrentNPCID => currentNPCID; @@ -354,11 +358,29 @@ public class NPCShopUIManager : AUIDialog // Create GShopItem GShopItem shopItem = CreateShopItemFromGood(good, itemData, itemDataType); - // Create panel with shop slot index (server expects this in npc_trade_item.index) - CreateItemPanel(shopItem, i); + // Create panel with absolute shop slot index (server validates npc_trade_item.index against full shop list). + int absoluteShopSlotIndex = GetAbsoluteShopSlotIndex(sellService, pageIndex, i); + CreateItemPanel(shopItem, absoluteShopSlotIndex); } } } + + private static int GetAbsoluteShopSlotIndex(NPC_SELL_SERVICE sellService, int pageIndex, int localIndex) + { + if (pageIndex <= 0) + return localIndex; + + int offset = 0; + int safePageIndex = Mathf.Clamp(pageIndex, 0, sellService.pages?.Length ?? 0); + for (int p = 0; p < safePageIndex; p++) + { + var goods = sellService.pages[p].goods; + if (goods != null) + offset += goods.Length; + } + + return offset + localIndex; + } GShopItem CreateShopItemFromGood(NPC_SELL_SERVICE.SellGood good, object itemData, DATA_TYPE itemDataType) { @@ -518,6 +540,15 @@ public class NPCShopUIManager : AUIDialog if (m_btn_buy != null) m_btn_buy.onClick.AddListener(OnBuyButtonClicked); + if (m_btn_reduce != null) + m_btn_reduce.onClick.AddListener(OnReduceBuyCountClicked); + + if (m_btn_incre != null) + m_btn_incre.onClick.AddListener(OnIncreaseBuyCountClicked); + + if (m_btn_max != null) + m_btn_max.onClick.AddListener(OnMaxBuyCountClicked); + if (m_btn_sell != null) m_btn_sell.onClick.AddListener(OnSellButtonClicked); @@ -542,6 +573,7 @@ public class NPCShopUIManager : AUIDialog if (currentItem.id == 0) { Debug.LogWarning("[NPCShopDetailPanel] Current item ID is 0, skipping display update"); + ResetBuyUi(); return; } @@ -550,23 +582,8 @@ public class NPCShopUIManager : AUIDialog itemDetailNameText.text = currentItem.name; } - uint price = 0; - if (currentItem.buy != null && currentItem.buy.Length > 0) - { - for (int i = 0; i < currentItem.buy.Length; i++) - { - if (currentItem.buy[i].price > 0) - { - price = currentItem.buy[i].price; - break; - } - } - } - - if(itemDetailPriceText != null) - { - itemDetailPriceText.text = price > 0 ? $"Giá: {price}" : "Price: N/A"; - } + buyCount = BuyCountMin; + UpdateBuyPriceTexts(); // Set item description if (itemDescriptionText != null) @@ -585,6 +602,148 @@ public class NPCShopUIManager : AUIDialog } } + private uint GetCurrentUnitPrice() + { + if (currentItem.id == 0) + return 0; + + if (currentItem.buy == null || currentItem.buy.Length == 0) + return 0; + + for (int i = 0; i < currentItem.buy.Length; i++) + { + if (currentItem.buy[i].price > 0) + return currentItem.buy[i].price; + } + + return 0; + } + + private int GetMaxBuyCountForCurrentItem() + { + if (currentItem.id == 0) + return BuyCountMin; + + int tid = (int)currentItem.id; + if (tid <= 0) + return BuyCountMin; + + int pileLimit = EC_IvtrItem.GetPileLimit(tid); + // In some setups the lightweight lookup returns 1 until the local DB data is loaded. + // If it says "1", try to resolve pile limit from a temporary item instance. + if (pileLimit <= BuyCountMin) + { + try + { + var tmp = EC_IvtrItem.CreateItem(tid, 0, 1); + if (tmp != null) + { + tmp.GetDetailDataFromLocal(); + int instLimit = tmp.GetPileLimitInstance(); + if (instLimit > pileLimit) + pileLimit = instLimit; + } + } + catch + { + // Keep the original pileLimit. + } + } + if (pileLimit < BuyCountMin) + pileLimit = BuyCountMin; + + return pileLimit; + } + + private void SetBuyCount(int newCount) + { + int max = GetMaxBuyCountForCurrentItem(); + if (newCount < BuyCountMin) newCount = BuyCountMin; + if (newCount > max) newCount = max; + + buyCount = newCount; + UpdateBuyPriceTexts(); + } + + private void UpdateBuyPriceTexts() + { + // Only update buy UI while buy panel is active. + if (contentMidBuy != null && !contentMidBuy.activeInHierarchy) + return; + + if (currentItem.id == 0) + { + ResetBuyUi(); + return; + } + + uint unitPrice = GetCurrentUnitPrice(); + int max = GetMaxBuyCountForCurrentItem(); + if (buyCount < BuyCountMin) buyCount = BuyCountMin; + if (buyCount > max) buyCount = max; + + // Quantity controls: + // - Non-stackable items have max == BuyCountMin, so keep +/-/max disabled. + // - Stackable items re-enable controls automatically. + bool canChangeQty = max > BuyCountMin; + if (m_btn_reduce != null) m_btn_reduce.interactable = canChangeQty && buyCount > BuyCountMin; + if (m_btn_incre != null) m_btn_incre.interactable = canChangeQty && buyCount < max; + if (m_btn_max != null) m_btn_max.interactable = canChangeQty && buyCount < max; + + long total = unitPrice > 0 ? (long)unitPrice * buyCount : 0; + if (total < 0) total = 0; + + if (itemsBuyAmountText != null) + { + itemsBuyAmountText.text = buyCount.ToString(); + } + + if (itemsBuyTotalMoneyText != null) + { + itemsBuyTotalMoneyText.text = total.ToString(); + } + + if (m_btn_buy != null) + { + m_btn_buy.interactable = unitPrice > 0 && buyCount >= BuyCountMin; + } + } + + private void ResetBuyUi() + { + buyCount = BuyCountMin; + if (itemsBuyAmountText != null) + itemsBuyAmountText.text = BuyUiEmptyValue; + if (itemsBuyTotalMoneyText != null) + itemsBuyTotalMoneyText.text = BuyUiEmptyValue; + if (m_btn_buy != null) + m_btn_buy.interactable = false; + if (m_btn_reduce != null) m_btn_reduce.interactable = false; + if (m_btn_incre != null) m_btn_incre.interactable = false; + if (m_btn_max != null) m_btn_max.interactable = false; + } + + private void OnReduceBuyCountClicked() + { + if (currentItem.id == 0) + return; + SetBuyCount(buyCount - 1); + } + + private void OnIncreaseBuyCountClicked() + { + if (currentItem.id == 0) + return; + SetBuyCount(buyCount + 1); + } + + private void OnMaxBuyCountClicked() + { + if (currentItem.id == 0) + return; + SetBuyCount(GetMaxBuyCountForCurrentItem()); + } + void LoadItemIcon(Image iconImage, int itemId) { if (itemId <= 0 || iconImage == null) @@ -670,35 +829,35 @@ public class NPCShopUIManager : AUIDialog } // Get price from item - uint price = 0; - if (currentItem.buy != null && currentItem.buy.Length > 0) + uint price = GetCurrentUnitPrice(); + if (price == 0) { - for (int i = 0; i < currentItem.buy.Length; i++) - { - if (currentItem.buy[i].price > 0) - { - price = currentItem.buy[i].price; - break; - } - } + Debug.LogWarning($"[NPCShopDetailPanel] Cannot buy item {currentItem.id} with invalid price"); + return; } + int max = GetMaxBuyCountForCurrentItem(); + if (buyCount < BuyCountMin) buyCount = BuyCountMin; + if (buyCount > max) buyCount = max; + // Server requires SEVNPC_HELLO with NPC id before buy, and the correct shop slot index if (shopManager != null && shopManager.CurrentNPCID != 0) UnityGameSession.c2s_CmdNPCSevHello((int)shopManager.CurrentNPCID); - // Create npc_trade_item: tid = template ID, index = shop slot (server validates this), count = quantity + // Create npc_trade_item: tid = template ID, index = shop slot (server validates this), count = quantity. + // For this server, multiple entries with the same index are rejected (ERROR_MESSAGE 15), + // so we must send a single entry and put the desired quantity in npc_trade_item.count. npc_trade_item[] items = new npc_trade_item[1]; items[0] = new npc_trade_item { tid = (int)currentItem.id, index = (uint)shopItemIndex, - count = 1 + count = (uint)buyCount }; - UnityGameSession.c2s_CmdNPCSevBuy(1, items); + UnityGameSession.c2s_CmdNPCSevBuy(items.Length, items); - Debug.Log($"[NPCShopDetailPanel] Sent buy command for item {currentItem.id}, price {price}"); + Debug.Log($"[NPCShopDetailPanel] Sent buy command for item {currentItem.id}, unitPrice {price}, count {buyCount}"); } private void OnSellButtonClicked() @@ -955,6 +1114,15 @@ public class NPCShopUIManager : AUIDialog if (m_btn_buy != null) m_btn_buy.onClick.RemoveListener(OnBuyButtonClicked); + if (m_btn_reduce != null) + m_btn_reduce.onClick.RemoveListener(OnReduceBuyCountClicked); + + if (m_btn_incre != null) + m_btn_incre.onClick.RemoveListener(OnIncreaseBuyCountClicked); + + if (m_btn_max != null) + m_btn_max.onClick.RemoveListener(OnMaxBuyCountClicked); + if (m_btn_sell != null) m_btn_sell.onClick.RemoveListener(OnSellButtonClicked); @@ -1044,6 +1212,7 @@ public class NPCShopUIManager : AUIDialog contentMidBuy.SetActive(true); contentRight.SetActive(true); contentMidSell.SetActive(false); + UpdateBuyPriceTexts(); } private void OnClickTabButtonSell() @@ -1058,42 +1227,7 @@ public class NPCShopUIManager : AUIDialog private void UpdateSellTotalPriceText() { - if (itemsPriceText == null) - return; - - if (sellSlotToSourceSlot == null || sellSlotToSourceSlot.Count == 0) - { - itemsPriceText.text = "0"; - return; - } - - var host = CECGameRun.Instance?.GetHostPlayer(); - var inv = host?.GetInventory(0); - if (inv == null) - { - itemsPriceText.text = "0"; - return; - } - - long total = 0; - foreach (var pair in sellSlotToSourceSlot) - { - int sourceSlot = pair.Value; - if (sourceSlot < 0 || sourceSlot >= inv.GetSize()) - continue; - - var item = inv.GetItem(sourceSlot, false); - if (item == null || item.m_iCount <= 0) - continue; - - if (!item.IsSellable()) - continue; - - item.GetDetailDataFromLocal(); - total += item.GetScaledPrice(); - } - - if (total < 0) total = 0; - itemsPriceText.text = total.ToString(); + // Sell total text removed from this manager. + return; } } diff --git a/Assets/Prefabs/UI/DialogNPCShop.prefab b/Assets/Prefabs/UI/DialogNPCShop.prefab index f1eacfd38e..7fa77b3efe 100644 --- a/Assets/Prefabs/UI/DialogNPCShop.prefab +++ b/Assets/Prefabs/UI/DialogNPCShop.prefab @@ -1368,7 +1368,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 0 + m_IsActive: 1 --- !u!224 &1538965918058098960 RectTransform: m_ObjectHideFlags: 0 @@ -12969,11 +12969,11 @@ MonoBehaviour: contentMidSell: {fileID: 5285943178504563476} item_info: {fileID: 3637242207861637912} itemDetailNameText: {fileID: 2529529646566217934} - itemDetailPriceText: {fileID: 2517942723267868233} - itemsPriceText: {fileID: 6499688482604231105} itemDescriptionText: legacy: {fileID: 0} tmp: {fileID: 5329995747664012504} + itemsBuyAmountText: {fileID: 1188040176770993507} + itemsBuyTotalMoneyText: {fileID: 1958673310957352830} tabButtonContainer: {fileID: 2298715577163083360} tabButtonPrefab: {fileID: 532136160345846687, guid: 548ae6ac061bc9648b093c9f9d203615, type: 3} tabButtonTextComponentName: Text From a33c35722bf21c8e86c4af4a053d6304ab0b32d9 Mon Sep 17 00:00:00 2001 From: VuNgocHaiC7 Date: Thu, 2 Apr 2026 17:57:25 +0700 Subject: [PATCH 6/6] update ui for dialogProduce.prefab --- .../Scripts/UI/Dialogs/DlgProduce.cs | 106 +- .../Scripts/UI/ProduceItemPanel.cs | 7 +- Assets/PerfectWorld/UI/DlgProduce.prefab | 19763 +++------------- Assets/Prefabs/UI/itemProduce.prefab | 328 +- 4 files changed, 3841 insertions(+), 16363 deletions(-) diff --git a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgProduce.cs b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgProduce.cs index 561c03662c..936bbfbb21 100644 --- a/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgProduce.cs +++ b/Assets/PerfectWorld/Scripts/UI/Dialogs/DlgProduce.cs @@ -28,7 +28,7 @@ namespace BrewMonster [SerializeField] private GameObject itemPb; [Header("Quantity")] - [SerializeField] private List quantityText; + [SerializeField] private TextMeshProUGUI quantityText; [SerializeField] private Button quantityIncreaseBtn; [SerializeField] private Button quantityDecreaseBtn; [SerializeField] private Button quantityMaxBtn; @@ -42,7 +42,7 @@ namespace BrewMonster [SerializeField] private List materialSlots = new List(); [Header("Result Slot")] - [SerializeField] private Transform itemResult; + [SerializeField] private Image itemResult; [Header("Item Info Panel")] public Transform itemInfoRoot; @@ -56,6 +56,9 @@ namespace BrewMonster [SerializeField] private TextMeshProUGUI weponDescInfoText; [SerializeField] private TextMeshProUGUI weponExtraInfoText; + [Header("Default")] + [SerializeField] private Sprite khung_item; + private NPC_MAKE_SERVICE? cachedMakeService = null; private int currentTabIndex = 0; private uint selectedRecipeId = 0; // Track the currently selected recipe @@ -79,8 +82,7 @@ namespace BrewMonster public override void Start() { - quantityText[0].text = currentQuantity.ToString(); - quantityText[1].text = currentQuantity.ToString(); + quantityText.text = currentQuantity.ToString(); quantityDecreaseBtn.onClick.AddListener(OnClickDecreaseBtn); quantityIncreaseBtn.onClick.AddListener(OnClickIncreaseBtn); quantityMaxBtn.onClick.AddListener(OnClickMaxBtn); @@ -309,8 +311,15 @@ namespace BrewMonster btn.onClick.RemoveAllListeners(); btn.onClick.AddListener(() => { - ShowItemInfoByRecipe(recipeId); + bool isNewRecipe = selectedRecipeId != recipeId; + selectedRecipeId = recipeId; + if (isNewRecipe) + { + currentQuantity = 1; + UpdateQuantityText(currentQuantity); + } ShowRecipeMaterials(recipeId); + ShowItemInfoByRecipe(recipeId); }); } @@ -331,14 +340,12 @@ namespace BrewMonster { if (slot == null) continue; - slot.gameObject.SetActive(false); - Transform iconTf = slot.Find("item"); if (iconTf != null) { Image img = iconTf.GetComponent(); if (img != null) - img.sprite = null; + img.sprite = khung_item; } Transform qtyTf = slot.Find("text_quantity"); @@ -352,30 +359,14 @@ namespace BrewMonster if (itemResult != null) { - itemResult.gameObject.SetActive(false); - - Transform iconTf = itemResult.Find("item"); - if (iconTf != null) - { - Image img = iconTf.GetComponent(); - if (img != null) - img.sprite = null; - } - - Transform qtyTf = itemResult.Find("text_quantity"); - if (qtyTf != null) - { - TextMeshProUGUI txt = qtyTf.GetComponent(); - if (txt != null) - txt.text = ""; - } + itemResult.sprite = khung_item; } } public void ShowRecipeMaterials(uint recipeId) { - selectedRecipeId = recipeId; // Track the selected recipe + selectedRecipeId = recipeId; ClearMaterialSlots(); var edm = ElementDataManProvider.GetElementDataMan(); @@ -395,32 +386,20 @@ namespace BrewMonster { uint outputItemId = recipe.targets[0].id_to_make; - itemResult.gameObject.SetActive(true); - - Transform iconTf = itemResult.Find("item"); - if (iconTf != null) + if (itemResult != null) { - Image img = iconTf.GetComponent(); - if (img != null) + if (itemResult != null) { Sprite sp = EC_IvtrItemUtils.Instance.ResolveItemIconSprite((int)outputItemId); if (sp != null) { - img.sprite = sp; - img.enabled = true; - img.preserveAspect = true; + itemResult.sprite = sp; + itemResult.enabled = true; + itemResult.preserveAspect = true; } } } - Transform qtyTf = itemResult.Find("text_quantity"); - if (qtyTf != null) - { - TextMeshProUGUI txt = qtyTf.GetComponent(); - if (txt != null) - txt.text = "1"; - } - Button resultBtn = itemResult.GetComponent