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