From 569d987dcf16e30e265ff0775af187d86327c159 Mon Sep 17 00:00:00 2001 From: HungDK <> Date: Thu, 2 Apr 2026 17:32:51 +0700 Subject: [PATCH 1/2] 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 2/2] 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