Merge pull request 'feature/update-ui' (#286) from feature/update-ui into develop

Reviewed-on: https://git.pthub.vn/Unity/perfect-world-unity/pulls/286
This commit is contained in:
hungdk
2026-04-06 04:29:01 +00:00
6 changed files with 205 additions and 74 deletions
@@ -159,6 +159,7 @@ namespace BrewMonster.Scripts.Managers
return true;
}
/// <summary>C++ <c>CECInventory::MergeItem</c>: walk slots, call <c>CECIvtrItem::MergeItem</c>, then new slot if needed.</summary>
public bool MergeItem(int tid, int iExpireDate, int iAmount, out int piLastSlot, out int piLastAmount)
{
piLastSlot = -1;
@@ -171,16 +172,8 @@ namespace BrewMonster.Scripts.Managers
var slotItem = m_aItems[i];
if (slotItem != null)
{
if (slotItem.GetTemplateID() != tid)
continue;
int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(tid));
int canAdd = Math.Max(0, pileLimit - Math.Max(0, slotItem.GetCount()));
if (canAdd <= 0) continue;
int add = Math.Min(canAdd, iAmount);
slotItem.AddAmount(add);
iAmount -= add;
int iNumMerge = slotItem.MergeItem(tid, iAmount);
iAmount -= iNumMerge;
if (iAmount == 0)
{
@@ -22,10 +22,15 @@ namespace BrewMonster.Scripts.Managers
{
[Header("Pack Buttons (assign in Inspector)")]
[SerializeField] private List<Button> inventoryPackButtons = new List<Button>(); // byPackage: 0
[Tooltip("Main slot grid: shows IVTRTYPE_PACK (0) on Item tab and IVTRTYPE_TASKPACK (2) on Task tab.")]
[SerializeField] private List<Button> inventoryPackButtons = new List<Button>();
[SerializeField] private List<Button> equipmentPackButtons = new List<Button>(); // byPackage: 1
[SerializeField] private List<Button> fashionPackButtons = new List<Button>(); // byPackage: 3
[Header("Inventory tabs — Item vs Task (original PW)")]
[SerializeField] private Button tabItemButton;
[SerializeField] private Button tabTaskButton;
[Header("Detail Panel (assign in Inspector)")]
[SerializeField] private ItemInfo detailPanelRoot;
[SerializeField] private Vector2 detailPanelOffset = new Vector2(20f, 0f);
@@ -89,14 +94,24 @@ namespace BrewMonster.Scripts.Managers
private EC_IvtrItem currentSelectedItem;
private EC_IvtrItem currentSelectedEquipment;
private const byte PKG_INVENTORY = 0;
private const byte PKG_EQUIPMENT = 1;
private const byte PKG_FASHION = 3; // Note: byPackage 3 used for Fashion
private const byte PKG_INVENTORY = InventoryConst.IVTRTYPE_PACK;
private const byte PKG_EQUIPMENT = InventoryConst.IVTRTYPE_EQUIPPACK;
private const byte PKG_TASK = InventoryConst.IVTRTYPE_TASKPACK;
private const byte PKG_FASHION = 3; // Trash / fashion box slot in legacy client (GetInventory may not resolve; see host)
public enum InventoryBagTab
{
Item,
Task,
}
private InventoryBagTab _bagTab = InventoryBagTab.Item;
private void Awake()
{
model = new InventoryModel();
view = new InventoryView();
WireBagTabButtons();
//if (currentDragImage == null)
//{
@@ -115,7 +130,7 @@ namespace BrewMonster.Scripts.Managers
private void Start()
{
RefreshAll();
SetBagTab(InventoryBagTab.Item);
if (hideDetailOnStart)
{
ShowDetailPanel(false);
@@ -130,6 +145,29 @@ namespace BrewMonster.Scripts.Managers
ApplyPendingCurrency();
UpdateCharacterInfo();
ShowDetailPanel(false);
RefreshAll();
}
private void WireBagTabButtons()
{
if (tabItemButton != null)
{
tabItemButton.onClick.RemoveAllListeners();
tabItemButton.onClick.AddListener(() => SetBagTab(InventoryBagTab.Item));
}
if (tabTaskButton != null)
{
tabTaskButton.onClick.RemoveAllListeners();
tabTaskButton.onClick.AddListener(() => SetBagTab(InventoryBagTab.Task));
}
}
/// <summary>Switches main bag view between normal items (tab 1) and task / quest bag (tab 2), like legacy PW.</summary>
public void SetBagTab(InventoryBagTab tab)
{
_bagTab = tab;
ShowDetailPanel(false);
RefreshAll();
}
private void Update()
@@ -149,9 +187,10 @@ namespace BrewMonster.Scripts.Managers
/// </summary>
public void UpdateCooldownOverlays()
{
// Update inventory pack cooldowns
// Main grid shows either normal pack or task pack depending on tab
// 更新背包冷却
UpdatePackageCooldowns(inventoryPackButtons, PKG_INVENTORY);
byte mainPack = _bagTab == InventoryBagTab.Item ? PKG_INVENTORY : PKG_TASK;
UpdatePackageCooldowns(inventoryPackButtons, mainPack);
}
/// <summary>
@@ -204,8 +243,16 @@ namespace BrewMonster.Scripts.Managers
var invItems = model.GetInventoryData(PKG_INVENTORY);
var eqpItems = model.GetInventoryData(PKG_EQUIPMENT);
var fshItems = model.GetInventoryData(PKG_FASHION);
var taskItems = model.GetInventoryData(PKG_TASK);
view.RenderPackage(inventoryPackButtons, invItems, PKG_INVENTORY, OnInventoryButtonClicked, GetDisplayTextForItem);
if (_bagTab == InventoryBagTab.Item)
{
view.RenderPackage(inventoryPackButtons, invItems, PKG_INVENTORY, OnInventoryButtonClicked, GetDisplayTextForItem);
}
else
{
view.RenderPackage(inventoryPackButtons, taskItems, PKG_TASK, OnInventoryButtonClicked, GetDisplayTextForItem);
}
view.RenderPackage(equipmentPackButtons, eqpItems, PKG_EQUIPMENT, OnInventoryButtonClicked, GetDisplayTextForItem);
view.RenderPackage(fashionPackButtons, fshItems, PKG_FASHION, OnInventoryButtonClicked, GetDisplayTextForItem);
UpdateCharacterInfo();
@@ -378,7 +425,7 @@ namespace BrewMonster.Scripts.Managers
return;
}
if (currentSelectedPackage == PKG_INVENTORY)
if (currentSelectedPackage == PKG_INVENTORY || currentSelectedPackage == PKG_TASK)
{
// Check if item is equipment
if (currentSelectedItem.IsEquipment())
@@ -415,9 +462,9 @@ namespace BrewMonster.Scripts.Managers
return;
}
if (currentSelectedPackage != PKG_INVENTORY)
if (currentSelectedPackage != PKG_INVENTORY && currentSelectedPackage != PKG_TASK)
{
Debug.LogWarning("[InventoryUI] Can only use items from inventory package");
Debug.LogWarning("[InventoryUI] Can only use items from inventory or task package");
return;
}
@@ -873,6 +920,23 @@ namespace BrewMonster.Scripts.Managers
}
}
/// <summary>
/// Stack count label under slot buttons: prefabs use <c>text_quatity</c> (typo) or <c>text_quantity</c>; legacy code used <c>text_quality</c>.
/// Only checks immediate children (Unity <see cref="Transform.Find"/>); deeper layouts fall back to <see cref="Component.GetComponentInChildren{T}"/> below.
/// </summary>
private static Transform FindStackCountTextTransform(Transform root)
{
if (root == null) return null;
string[] names = { "text_quality", "text_quatity", "text_quantity" };
for (int n = 0; n < names.Length; n++)
{
var t = root.Find(names[n]);
if (t != null) return t;
}
return null;
}
/// <summary>
/// Update or create text component to show item count on the button
/// </summary>
@@ -885,22 +949,19 @@ namespace BrewMonster.Scripts.Managers
TMPro.TextMeshProUGUI tmpText = null;
Text legacyText = null;
// Find text component
var textTransform = button.transform.Find("text_quality");
var textTransform = FindStackCountTextTransform(button.transform);
if (textTransform != null)
{
tmpText = textTransform.GetComponent<TMPro.TextMeshProUGUI>();
legacyText = textTransform.GetComponent<Text>();
}
else
if (tmpText == null && legacyText == null)
{
// Fallback: find any text in children
tmpText = button.GetComponentInChildren<TMPro.TextMeshProUGUI>();
if (tmpText == null)
{
legacyText = button.GetComponentInChildren<Text>();
}
}
// Update text
@@ -950,6 +1011,7 @@ namespace BrewMonster.Scripts.Managers
switch (package)
{
case PKG_INVENTORY:
case PKG_TASK:
list = inventoryPackButtons;
break;
case PKG_EQUIPMENT:
@@ -1061,7 +1123,7 @@ namespace BrewMonster.Scripts.Managers
var tmpText = equipButton.GetComponentInChildren<TMPro.TextMeshProUGUI>();
if (tmpText != null)
{
if (package == PKG_INVENTORY)
if (package == PKG_INVENTORY || package == PKG_TASK)
{
//if item is @EC_IvtrEquip and is not equipped, show equip button
if(item is EC_IvtrEquip)
@@ -1088,7 +1150,7 @@ namespace BrewMonster.Scripts.Managers
}
else
{
if (package == PKG_INVENTORY)
if (package == PKG_INVENTORY || package == PKG_TASK)
{
if(item is EC_IvtrEquip)
{
@@ -1211,14 +1211,12 @@ namespace BrewMonster.Scripts.Managers
return iNumAdd;
}
/// <summary>Add item amount. Returns new amount of item.</summary>
/// <summary>Add item amount. Returns new count (C++ <c>a_Clamp</c> to <c>m_iPileLimit</c>).</summary>
public int AddAmount(int iAmount)
{
Debug.Log($"[EC_IvtrItem] Old Amount: {m_iCount}");
m_iCount += iAmount;
if (m_iCount < 0) m_iCount = 0;
//if (m_iCount > m_iPileLimit) m_iCount = m_iPileLimit;
Debug.Log($"[EC_IvtrItem] New Amount: {m_iCount}");
if (m_iCount > m_iPileLimit) m_iCount = m_iPileLimit;
return m_iCount;
}
+18 -19
View File
@@ -1961,7 +1961,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!224 &64585481268713917
RectTransform:
m_ObjectHideFlags: 0
@@ -1977,9 +1977,9 @@ RectTransform:
- {fileID: 7063925482172052076}
m_Father: {fileID: 4359352035478671586}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 92.65, y: -36.303}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 185.3, y: 72.606}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6580926924470282336
@@ -10280,7 +10280,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!224 &7214549036987193986
RectTransform:
m_ObjectHideFlags: 0
@@ -10296,9 +10296,9 @@ RectTransform:
- {fileID: 8707107604074202174}
m_Father: {fileID: 4359352035478671586}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 463.25, y: -36.30285}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 185.3, y: 72.6057}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6248269220837765992
@@ -10322,7 +10322,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Color: {r: 1, g: 1, b: 1, a: 0.33333334}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -10378,7 +10378,7 @@ MonoBehaviour:
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_Interactable: 0
m_TargetGraphic: {fileID: 8383648154857910679}
m_OnClick:
m_PersistentCalls:
@@ -12562,6 +12562,8 @@ MonoBehaviour:
- {fileID: 6415804096478650164}
- {fileID: 486249631205428665}
fashionPackButtons: []
tabItemButton: {fileID: 1025193074652694500}
tabTaskButton: {fileID: 8447324723389536767}
detailPanelRoot: {fileID: 759109931263093524}
detailPanelOffset: {x: 20, y: 0}
hideDetailOnStart: 1
@@ -12576,9 +12578,6 @@ MonoBehaviour:
moneyTextsLegacy: []
moneyTextsTMP:
- {fileID: 776624419558043962}
cashTextsLegacy: []
cashTextsTMP:
- {fileID: 0}
characterNameTextLegacy: {fileID: 0}
characterNameTextTMP: {fileID: 6101848964131719891}
characterLevelTextLegacy: {fileID: 0}
@@ -14589,7 +14588,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!224 &6872360865646447011
RectTransform:
m_ObjectHideFlags: 0
@@ -14605,9 +14604,9 @@ RectTransform:
- {fileID: 3208901167605625288}
m_Father: {fileID: 4359352035478671586}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 277.95, y: -36.30285}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 185.3, y: 72.6057}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8787094950878048123
@@ -15483,8 +15482,8 @@ MonoBehaviour:
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
rgba: 1677721599
m_fontColor: {r: 1, g: 1, b: 1, a: 0.3882353}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
@@ -482,6 +482,47 @@ namespace BrewMonster
}
}
}
/// <summary>C++ CECDealInventory::GetItemIndexByFlag</summary>
public int GetItemIndexByFlag(int iFlag)
{
int n = GetSize();
for (int i = 0; i < n; i++)
{
if (GetItem(i, false) != null && m_aItemInfo[i].iFlag == iFlag)
return i;
}
return -1;
}
/// <summary>C++ CECDealInventory::RemoveItemByFlag</summary>
public void RemoveItemByFlag(int iFlag, int iAmount)
{
int n = GetSize();
for (int i = 0; i < n; i++)
{
var slotItem = GetItem(i, false);
if (slotItem == null)
continue;
var info = m_aItemInfo[i];
if (info.iFlag != iFlag)
continue;
if (iAmount < 0 || iAmount >= info.iAmount)
{
SetItem(i, null);
}
else
{
info.iAmount -= iAmount;
m_aItemInfo[i] = info;
if (info.bDelete)
slotItem.AddAmount(-iAmount);
}
return;
}
}
}
// ̯ƱԶת
+60 -22
View File
@@ -594,7 +594,13 @@ namespace BrewMonster
UpdateEquipSkins();
}
/// <summary>Buy from NPC/booth: server sends PURCHASE_ITEM (cmd_purchase_item). C++ OnMsgHstPurchaseItems.</summary>
/// <summary>
/// Buy from NPC/booth: S2C PURCHASE_ITEM (<c>cmd_purchase_item</c>).
/// Port of <c>CECHostPlayer::OnMsgHstPurchaseItems</c> (EC_HostMsg.cpp): prefer <c>MergeItem</c> like the C++ client.
/// If merge lands on a different slot than <c>inv_index</c> (common when stacking into an earlier pile), we still accept
/// the merged slot—the strict C++ <c>iLastSlot == inv_index</c> check would skip updates and break buy/stack UI here.
/// Fallback: <c>PutItemInSlot(inv_index)</c> then <c>MergeItem</c> when merge alone fails.
/// </summary>
public void OnMsgHstPurchaseItems(ECMSG Msg)
{
var data = Msg.dwParam1 as byte[];
@@ -609,6 +615,7 @@ namespace BrewMonster
return;
var slotsNeedingDetail = new System.Collections.Generic.List<byte>();
bool purchaseSlotMismatch = false;
for (int i = 0; i < header.item_count && index + itemSize <= data.Length; i++)
{
@@ -616,37 +623,58 @@ namespace BrewMonster
int expire_date = BitConverter.ToInt32(data, index); index += 4;
int count = (int)BitConverter.ToUInt32(data, index); index += 4;
ushort inv_index = BitConverter.ToUInt16(data, index); index += 2;
index += 1; // booth_slot
byte booth_slot = data[index];
index += 1;
if (inv_index >= pPack.GetSize())
pPack.Resize(inv_index + 1);
bool placed = pPack.PutItemInSlot(inv_index, item_id, expire_date, count, out int lastSlot, out int slotNum);
int iLastSlot = 0;
int iSlotNum = 0;
bool placed = pPack.MergeItem(item_id, expire_date, count, out iLastSlot, out iSlotNum);
if (!placed)
placed = pPack.PutItemInSlot(inv_index, item_id, expire_date, count, out iLastSlot, out iSlotNum);
if (!placed)
placed = pPack.MergeItem(item_id, expire_date, count, out iLastSlot, out iSlotNum);
if (!placed)
{
placed = pPack.MergeItem(item_id, expire_date, count, out lastSlot, out slotNum);
if (!placed || lastSlot != inv_index)
continue;
#if UNITY_EDITOR
UnityEngine.Debug.LogWarning(
$"[OnMsgHstPurchaseItems] Could not place purchase tid={item_id} count={count} inv_index={inv_index}");
#endif
continue;
}
var pItem = pPack.GetItem(inv_index, false);
if (iLastSlot != inv_index)
purchaseSlotMismatch = true;
#if UNITY_EDITOR
if (iLastSlot != inv_index)
{
UnityEngine.Debug.Log(
$"[OnMsgHstPurchaseItems] Using merge slot {iLastSlot} (packet inv_index={inv_index}) tid={item_id}");
}
#endif
var pItem = pPack.GetItem(iLastSlot, false);
if (pItem != null)
{
pItem.Package = (byte)Inventory_type.IVTRTYPE_PACK;
pItem.Slot = inv_index;
int cid = pItem.GetClassID();
if (pItem.IsEquipment() ||
cid == (int)EC_IvtrItem.InventoryClassId.ICID_TASKNMMATTER ||
cid == (int)EC_IvtrItem.InventoryClassId.ICID_TASKDICE ||
cid == (int)EC_IvtrItem.InventoryClassId.ICID_TASKITEM ||
cid == (int)EC_IvtrItem.InventoryClassId.ICID_GOBLIN_EXPPILL ||
cid == (int)EC_IvtrItem.InventoryClassId.ICID_WEDDINGBOOKCARD ||
cid == (int)EC_IvtrItem.InventoryClassId.ICID_WEDDINGINVITECARD ||
cid == (int)EC_IvtrItem.InventoryClassId.ICID_SKILLTOME ||
cid == (int)EC_IvtrItem.InventoryClassId.ICID_GOBLIN ||
cid == (int)EC_IvtrItem.InventoryClassId.ICID_PETEGG)
pItem.Slot = iLastSlot;
// Keep stack size in sync with MergeItem/PutItemInSlot totals (authoritative for this slot).
if (iSlotNum > 0)
pItem.SetCount(iSlotNum);
if (pItem.IsEquipment())
slotsNeedingDetail.Add((byte)iLastSlot);
}
if (header.flag != 0 && GetBoothState() == 2)
{
var boothBPack = GetBoothBuyPack();
if (boothBPack != null)
{
slotsNeedingDetail.Add((byte)inv_index);
// C++: CDlgInfo FIXMSG_BOOTHBUY — no matching in-game UI hook here; update booth pack only.
boothBPack.RemoveItemByFlag(booth_slot, count);
}
}
}
@@ -656,8 +684,18 @@ namespace BrewMonster
foreach (byte slot in slotsNeedingDetail)
UnityGameSession.c2s_CmdGetItemInfo((byte)Inventory_type.IVTRTYPE_PACK, slot);
var ui = GameObject.FindFirstObjectByType<EC_InventoryUI>();
ui?.RefreshAll();
{
var ui = GameObject.FindFirstObjectByType<EC_InventoryUI>();
ui?.RefreshAll();
if (purchaseSlotMismatch)
{
UnityGameSession.RequestInventoryAsync(0, () =>
{
var ui2 = GameObject.FindFirstObjectByType<EC_InventoryUI>();
ui2?.RefreshAll();
});
}
}
UpdateEquipSkins();
}