1776 lines
67 KiB
C#
1776 lines
67 KiB
C#
using BrewMonster;
|
|
using BrewMonster.Common;
|
|
using BrewMonster.Network;
|
|
using BrewMonster.Scripts;
|
|
using BrewMonster.Scripts.UI;
|
|
using BrewMonster.Scripts.Task.UI;
|
|
using BrewMonster.UI;
|
|
using CSNetwork.GPDataType;
|
|
using ModelRenderer.Scripts.GameData;
|
|
using PerfectWorld.Scripts.Managers;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
|
|
namespace BrewMonster.Scripts.Managers
|
|
{
|
|
public class EC_InventoryUI : AUIDialog
|
|
{
|
|
|
|
[Header("Pack Buttons (assign in Inspector)")]
|
|
[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);
|
|
[SerializeField] private bool hideDetailOnStart = true;
|
|
[SerializeField] private EC_UIUtility.TextOutlet descriptionText;
|
|
[SerializeField] private Button equipButton;
|
|
[SerializeField] private Button dropButton;
|
|
|
|
[Header("Stack Split UI (assign in Inspector)")]
|
|
[SerializeField] private GameObject splitPanelRoot;
|
|
[SerializeField] private TMPro.TMP_InputField splitAmountText;
|
|
[SerializeField] private Button splitConfirmButton;
|
|
[SerializeField] private Button splitCloseButton;
|
|
[SerializeField] private Button splitOpenButton;
|
|
[SerializeField] private Button splitIncreaseButton;
|
|
[SerializeField] private Button splitDecreaseButton;
|
|
[SerializeField] private Button splitMaxButton;
|
|
|
|
[Header("Stack combine — merge into another stack (C++ inventory drag-merge, assign in Inspector)")]
|
|
[SerializeField] private Button combineStackButton;
|
|
|
|
private int _splitAmount = 1;
|
|
private int _splitMaxAmount = 1;
|
|
|
|
[Header("Inventory Settings")]
|
|
[SerializeField] private bool autoRefresh = true;
|
|
[SerializeField] private float refreshInterval = 1.0f;
|
|
[SerializeField] private bool showEquipmentDetails = true;
|
|
|
|
[Header("Money UI (assign any text fields to mirror money amount)")]
|
|
[SerializeField] private List<UnityEngine.UI.Text> moneyTextsLegacy = new List<UnityEngine.UI.Text>();
|
|
[SerializeField] private List<TMPro.TextMeshProUGUI> moneyTextsTMP = new List<TMPro.TextMeshProUGUI>();
|
|
|
|
[Header("Character UI (assign in Inspector)")]
|
|
[SerializeField] private UnityEngine.UI.Text characterNameTextLegacy;
|
|
[SerializeField] private TMPro.TextMeshProUGUI characterNameTextTMP;
|
|
[SerializeField] private UnityEngine.UI.Text characterLevelTextLegacy;
|
|
[SerializeField] private TMPro.TextMeshProUGUI characterLevelTextTMP;
|
|
|
|
private float lastRefreshTime;
|
|
|
|
// Pending currency cache for when UI is not yet active
|
|
private static bool s_hasPendingMoney;
|
|
private static ulong s_pendingMoneyAmount;
|
|
private static ulong s_pendingMoneyMaxAmount;
|
|
|
|
// Flags to prevent log spam for extended description warnings
|
|
// 防止扩展描述警告日志刷屏的标志
|
|
private static bool m_HasLoggedExtDescNull = false;
|
|
private static bool m_HasLoggedExtDescNotInit = false;
|
|
private static bool m_HasLoggedExtDescError = false;
|
|
|
|
private InventoryModel model;
|
|
private InventoryView view;
|
|
|
|
//// Drag-and-drop state
|
|
//private int draggedItemSourceSlot = -1;
|
|
//private byte draggedItemSourcePackage = 0;
|
|
//[SerializeField] private Image currentDragImage;
|
|
//private bool isDragging = false;
|
|
|
|
// === Text Formatting Methods ===
|
|
|
|
/// <summary>
|
|
/// Format text for TextMeshPro components with rich text support
|
|
/// </summary>
|
|
/// <param name="text">Raw text with formatting codes</param>
|
|
/// <returns>Formatted text for TextMeshPro</returns>
|
|
private static string FormatForTextMeshPro(string text)
|
|
{
|
|
return EC_Utility.FormatForTextMeshPro(text);
|
|
}
|
|
|
|
// Current selected item for equip/unequip operations
|
|
private byte currentSelectedPackage;
|
|
private int currentSelectedSlot;
|
|
private EC_IvtrItem currentSelectedItem;
|
|
private EC_IvtrItem currentSelectedEquipment;
|
|
|
|
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();
|
|
WireSplitUI();
|
|
WireCombineUI();
|
|
|
|
//if (currentDragImage == null)
|
|
//{
|
|
// var canvas = GetComponentInParent<Canvas>();
|
|
// if (canvas == null)
|
|
// {
|
|
// canvas = FindAnyObjectByType<Canvas>();
|
|
// }
|
|
// var go = new GameObject("DragImage", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
|
// go.transform.SetParent(canvas.transform, false);
|
|
// currentDragImage = go.GetComponent<Image>();
|
|
// currentDragImage.raycastTarget = false;
|
|
// currentDragImage.gameObject.SetActive(false);
|
|
//}
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
SetBagTab(InventoryBagTab.Item);
|
|
if (hideDetailOnStart)
|
|
{
|
|
ShowDetailPanel(false);
|
|
}
|
|
ShowSplitPanel(false);
|
|
// Apply any pending currency values captured before the UI became active
|
|
ApplyPendingCurrency();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
// Ensure cached values are pushed when the UI is enabled
|
|
ApplyPendingCurrency();
|
|
UpdateCharacterInfo();
|
|
ShowDetailPanel(false);
|
|
ShowSplitPanel(false);
|
|
RefreshAll();
|
|
}
|
|
|
|
private void WireSplitUI()
|
|
{
|
|
if (splitOpenButton != null)
|
|
{
|
|
splitOpenButton.onClick.RemoveAllListeners();
|
|
splitOpenButton.onClick.AddListener(OpenSplitPanelForSelection);
|
|
}
|
|
if (splitConfirmButton != null)
|
|
{
|
|
splitConfirmButton.onClick.RemoveAllListeners();
|
|
splitConfirmButton.onClick.AddListener(ConfirmSplit);
|
|
}
|
|
if (splitCloseButton != null)
|
|
{
|
|
splitCloseButton.onClick.RemoveAllListeners();
|
|
splitCloseButton.onClick.AddListener(() => ShowSplitPanel(false));
|
|
}
|
|
|
|
if (splitIncreaseButton != null)
|
|
{
|
|
splitIncreaseButton.onClick.RemoveAllListeners();
|
|
splitIncreaseButton.onClick.AddListener(() => SetSplitAmount(_splitAmount + 1));
|
|
}
|
|
|
|
if (splitDecreaseButton != null)
|
|
{
|
|
splitDecreaseButton.onClick.RemoveAllListeners();
|
|
splitDecreaseButton.onClick.AddListener(() => SetSplitAmount(_splitAmount - 1));
|
|
}
|
|
|
|
if (splitMaxButton != null)
|
|
{
|
|
splitMaxButton.onClick.RemoveAllListeners();
|
|
splitMaxButton.onClick.AddListener(() => SetSplitAmount(_splitMaxAmount));
|
|
}
|
|
|
|
if (splitAmountText != null)
|
|
{
|
|
splitAmountText.onValueChanged.RemoveAllListeners();
|
|
splitAmountText.onValueChanged.AddListener(OnSplitAmountInputChanged);
|
|
}
|
|
}
|
|
|
|
private void WireCombineUI()
|
|
{
|
|
if (combineStackButton != null)
|
|
{
|
|
combineStackButton.onClick.RemoveAllListeners();
|
|
combineStackButton.onClick.AddListener(() => CombineSelectedStack());
|
|
}
|
|
}
|
|
|
|
private void ShowSplitPanel(bool show)
|
|
{
|
|
if (splitPanelRoot != null)
|
|
splitPanelRoot.SetActive(show);
|
|
if (IsSplitCloseButtonOnModal() && splitCloseButton != null)
|
|
splitCloseButton.gameObject.SetActive(show);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call this from your UI (or bind it to a button) to open the split panel for current selection.
|
|
/// </summary>
|
|
public void OpenSplitPanelForSelection()
|
|
{
|
|
if (currentSelectedItem == null || currentSelectedPackage != PKG_INVENTORY)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] OpenSplitPanelForSelection: select an inventory stack first");
|
|
return;
|
|
}
|
|
|
|
int total = currentSelectedItem.m_iCount;
|
|
if (total <= 1)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] OpenSplitPanelForSelection: item count <= 1");
|
|
return;
|
|
}
|
|
|
|
_splitMaxAmount = Math.Max(1, total - 1);
|
|
_splitAmount = Mathf.Clamp(_splitAmount, 1, _splitMaxAmount);
|
|
UpdateSplitAmountUI();
|
|
ShowSplitPanel(true);
|
|
}
|
|
|
|
private void SetSplitAmount(int amount)
|
|
{
|
|
_splitAmount = Mathf.Clamp(amount, 1, Math.Max(1, _splitMaxAmount));
|
|
UpdateSplitAmountUI();
|
|
}
|
|
|
|
private void UpdateSplitAmountUI()
|
|
{
|
|
if (splitAmountText != null)
|
|
splitAmountText.SetTextWithoutNotify(_splitAmount.ToString());
|
|
|
|
if (splitIncreaseButton != null)
|
|
splitIncreaseButton.interactable = _splitAmount < _splitMaxAmount;
|
|
if (splitDecreaseButton != null)
|
|
splitDecreaseButton.interactable = _splitAmount > 1;
|
|
if (splitMaxButton != null)
|
|
splitMaxButton.interactable = _splitAmount < _splitMaxAmount;
|
|
}
|
|
|
|
private void OnSplitAmountInputChanged(string raw)
|
|
{
|
|
if (!int.TryParse(raw, out int v))
|
|
return;
|
|
|
|
v = Mathf.Clamp(v, 1, Math.Max(1, _splitMaxAmount));
|
|
if (v == _splitAmount)
|
|
return;
|
|
|
|
_splitAmount = v;
|
|
UpdateSplitAmountUI();
|
|
}
|
|
|
|
private void ConfirmSplit()
|
|
{
|
|
int amount = _splitAmount;
|
|
if (amount <= 0) return;
|
|
if (SeparateSelectedStack(amount))
|
|
{
|
|
ShowSplitPanel(false);
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
if (autoRefresh && Time.time - lastRefreshTime >= refreshInterval)
|
|
{
|
|
RefreshAll();
|
|
}
|
|
|
|
UpdateCooldownOverlays();
|
|
HandleDetailPanelDismissInput();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update all cooldown overlays
|
|
/// 更新所有冷却遮罩
|
|
/// </summary>
|
|
public void UpdateCooldownOverlays()
|
|
{
|
|
// Main grid shows either normal pack or task pack depending on tab
|
|
// 更新背包冷却
|
|
byte mainPack = _bagTab == InventoryBagTab.Item ? PKG_INVENTORY : PKG_TASK;
|
|
UpdatePackageCooldowns(inventoryPackButtons, mainPack);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update cooldown overlays for a specific package
|
|
/// 更新特定包裹的冷却遮罩
|
|
/// </summary>
|
|
private void UpdatePackageCooldowns(List<Button> buttons, byte package)
|
|
{
|
|
if (buttons == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var items = model.GetInventoryData(package);
|
|
if (items == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int slot = 0; slot < buttons.Count; slot++)
|
|
{
|
|
var button = buttons[slot];
|
|
if (button == null)
|
|
continue;
|
|
|
|
// Get item at this slot
|
|
// 获取此槽位的物品
|
|
EC_IvtrItem itemData = null;
|
|
bool hasItem = items.TryGetValue(slot, out itemData) && itemData != null;
|
|
|
|
if (hasItem)
|
|
{
|
|
// Use InventoryView's method to update cooldown
|
|
// 使用 InventoryView 的方法更新冷却
|
|
view.UpdateCooldownOverlay(button, itemData);
|
|
}
|
|
else
|
|
{
|
|
//// Hide overlay for empty slots
|
|
//// 空槽位隐藏遮罩
|
|
//view.HideCooldownOverlay(button);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void RefreshAll()
|
|
{
|
|
lastRefreshTime = Time.time;
|
|
|
|
var invItems = model.GetInventoryData(PKG_INVENTORY);
|
|
var eqpItems = model.GetInventoryData(PKG_EQUIPMENT);
|
|
var fshItems = model.GetInventoryData(PKG_FASHION);
|
|
var taskItems = model.GetInventoryData(PKG_TASK);
|
|
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update all configured money text components with the current amount.
|
|
/// Call this when GET_OWN_MONEY arrives.
|
|
/// </summary>
|
|
public void UpdateMoney(ulong amount, ulong maxAmount)
|
|
{
|
|
s_pendingMoneyAmount = amount;
|
|
s_pendingMoneyMaxAmount = maxAmount;
|
|
s_hasPendingMoney = true;
|
|
string text = amount.ToString();
|
|
if (moneyTextsLegacy != null)
|
|
{
|
|
for (int i = 0; i < moneyTextsLegacy.Count; i++)
|
|
{
|
|
var t = moneyTextsLegacy[i];
|
|
if (t != null) t.text = text;
|
|
}
|
|
}
|
|
if (moneyTextsTMP != null)
|
|
{
|
|
for (int i = 0; i < moneyTextsTMP.Count; i++)
|
|
{
|
|
var t = moneyTextsTMP[i];
|
|
if (t != null) t.text = text;
|
|
Debug.Log($"[InventoryUI] Updated money text to {t.text} in TextMeshPro component {t?.name}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateCharacterInfo()
|
|
{
|
|
var host = CECGameRun.Instance?.GetHostPlayer();
|
|
string characterName = string.Empty;
|
|
string characterLevel = string.Empty;
|
|
|
|
if (host != null)
|
|
{
|
|
characterName = host.GetName() ?? string.Empty;
|
|
characterLevel = host.GetBasicProps().iLevel.ToString();
|
|
}
|
|
|
|
if (characterNameTextLegacy != null)
|
|
characterNameTextLegacy.text = characterName;
|
|
if (characterNameTextTMP != null)
|
|
characterNameTextTMP.text = characterName;
|
|
if (characterLevelTextLegacy != null)
|
|
characterLevelTextLegacy.text = characterLevel;
|
|
if (characterLevelTextTMP != null)
|
|
characterLevelTextTMP.text = characterLevel;
|
|
}
|
|
|
|
// Public static entry points to cache values when UI is unavailable
|
|
public static void CacheMoney(ulong amount, ulong maxAmount)
|
|
{
|
|
s_pendingMoneyAmount = amount;
|
|
s_pendingMoneyMaxAmount = maxAmount;
|
|
s_hasPendingMoney = true;
|
|
// If an instance exists (even inactive), push immediately so the value is ready
|
|
var all = Resources.FindObjectsOfTypeAll<EC_InventoryUI>();
|
|
if (all != null)
|
|
{
|
|
for (int i = 0; i < all.Length; i++)
|
|
{
|
|
var ui = all[i];
|
|
if (ui != null && ui.gameObject.scene.IsValid())
|
|
{
|
|
ui.ApplyPendingCurrency();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplyPendingCurrency()
|
|
{
|
|
if (s_hasPendingMoney)
|
|
{
|
|
UpdateMoney(s_pendingMoneyAmount, s_pendingMoneyMaxAmount);
|
|
}
|
|
}
|
|
|
|
private void OnInventoryButtonClicked(byte package, int slot)
|
|
{
|
|
UnityGameSession.RequestCheckSecurityPassWd("");
|
|
var data = model.GetInventoryData(package);
|
|
if (data != null && data.TryGetValue(slot, out var itemData))
|
|
{
|
|
// Store current selection for equip/unequip operations
|
|
currentSelectedPackage = package;
|
|
currentSelectedSlot = slot;
|
|
currentSelectedItem = itemData;
|
|
|
|
// Create equipment object if this is equipment
|
|
currentSelectedEquipment = CreateEquipmentFromItemData(itemData);
|
|
|
|
// Position detail panel near the clicked item button
|
|
|
|
FillDetailPanel(package, itemData);
|
|
PositionDetailPanelNearButton(package, slot);
|
|
}
|
|
else
|
|
{
|
|
ShowDetailPanel(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create EC_IvtrEquip object from InventoryItemData
|
|
/// </summary>
|
|
private EC_IvtrItem CreateEquipmentFromItemData(EC_IvtrItem itemData)
|
|
{
|
|
if (itemData == null)
|
|
return null;
|
|
|
|
var equipment = EC_IvtrItem.CreateItem(itemData.m_tid, itemData.m_expire_date, itemData.m_iCount);
|
|
|
|
// Parse item info if available (use Content field)
|
|
if (itemData.Content != null && itemData.Content.Length > 0)
|
|
{
|
|
equipment.SetItemInfo(itemData.Content, itemData.Content.Length);
|
|
}
|
|
|
|
return equipment;
|
|
}
|
|
|
|
private string GetDisplayTextForItem(int slot, EC_IvtrItem itemData)
|
|
{
|
|
if (itemData == null || itemData.m_iCount <= 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(itemData.m_tid);
|
|
string displayText = string.IsNullOrEmpty(itemName) ? $"Item {itemData.m_tid}" : itemName;
|
|
if (itemData.m_iCount > 1)
|
|
{
|
|
displayText += $" x{itemData.m_iCount}";
|
|
}
|
|
return displayText;
|
|
}
|
|
|
|
public void ToggleAutoRefresh()
|
|
{
|
|
autoRefresh = !autoRefresh;
|
|
}
|
|
|
|
public void SetRefreshInterval(float interval)
|
|
{
|
|
refreshInterval = Mathf.Max(0.1f, interval);
|
|
}
|
|
|
|
public void ToggleEquipmentDetails()
|
|
{
|
|
showEquipmentDetails = !showEquipmentDetails;
|
|
if (currentSelectedItem != null)
|
|
{
|
|
FillDetailPanel(currentSelectedPackage, currentSelectedItem);
|
|
}
|
|
}
|
|
|
|
public void OnEquipButtonClicked()
|
|
{
|
|
if (currentSelectedItem == null)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] No item selected for operation");
|
|
return;
|
|
}
|
|
|
|
if (currentSelectedPackage == PKG_INVENTORY || currentSelectedPackage == PKG_TASK)
|
|
{
|
|
// Check if item is equipment
|
|
if (currentSelectedItem.IsEquipment())
|
|
{
|
|
// Equipping from inventory
|
|
EquipItem();
|
|
}
|
|
else
|
|
{
|
|
// Use item from inventory
|
|
UseItem();
|
|
}
|
|
}
|
|
else if (currentSelectedPackage == PKG_EQUIPMENT)
|
|
{
|
|
// Unequipping from equipment
|
|
UnequipItem();
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[InventoryUI] Operation not supported for package {currentSelectedPackage}");
|
|
}
|
|
detailPanelRoot.gameObject.SetActive(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Use the currently selected item from inventory
|
|
/// </summary>
|
|
private void UseItem()
|
|
{
|
|
if (currentSelectedItem == null)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] No item selected for use");
|
|
return;
|
|
}
|
|
|
|
if (currentSelectedPackage != PKG_INVENTORY && currentSelectedPackage != PKG_TASK)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] Can only use items from inventory or task package");
|
|
return;
|
|
}
|
|
|
|
Debug.Log($"[UseItem] Attempting to use item {currentSelectedItem.m_tid} from slot {currentSelectedSlot}");
|
|
|
|
// Get host player to call UseItemInPack
|
|
var host = CECGameRun.Instance?.GetHostPlayer();
|
|
if (host == null)
|
|
{
|
|
Debug.LogError("[InventoryUI] Cannot get host player");
|
|
return;
|
|
}
|
|
|
|
// Call UseItemInPack with current package and slot
|
|
bool success = host.UseItemInPack(currentSelectedPackage, currentSelectedSlot, true);
|
|
|
|
if (success)
|
|
{
|
|
Debug.Log($"[UseItem] Successfully used item {currentSelectedItem.m_tid} from slot {currentSelectedSlot}");
|
|
|
|
// Close detail panel after using item
|
|
ShowDetailPanel(false);
|
|
|
|
// Refresh inventory to reflect changes
|
|
RefreshAll();
|
|
|
|
Debug.Log($"[UseItem] Calling UpdateCooldownOverlays after item use");
|
|
UpdateCooldownOverlays();
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[UseItem] Failed to use item {currentSelectedItem.m_tid} from slot {currentSelectedSlot}");
|
|
}
|
|
}
|
|
|
|
public void OnDropButtonClicked()
|
|
{
|
|
if (currentSelectedItem == null)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] No item selected for drop operation");
|
|
return;
|
|
}
|
|
|
|
if (currentSelectedPackage == PKG_INVENTORY)
|
|
{
|
|
// Dropping from inventory
|
|
DropInventoryItem();
|
|
}
|
|
else if (currentSelectedPackage == PKG_EQUIPMENT)
|
|
{
|
|
// Dropping from equipment
|
|
DropEquipItem();
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[InventoryUI] Drop not supported for package {currentSelectedPackage}");
|
|
}
|
|
detailPanelRoot.gameObject.SetActive(false);
|
|
}
|
|
|
|
private void EquipItem()
|
|
{
|
|
if (currentSelectedItem == null) return;
|
|
|
|
// For equipping, we need to find an empty equipment slot
|
|
// Use the new method that checks for available slots (especially for finger items)
|
|
byte equipLocation = EC_IvtrType.GetAvailableEquipLocationForItem(currentSelectedItem.m_tid);
|
|
if (equipLocation >= (byte)IndexOfIteminEquipmentInventory.SIZE_EQUIPIVTR)
|
|
{
|
|
Debug.LogWarning($"[InventoryUI] Could not determine equip location for item {currentSelectedItem.m_tid}");
|
|
return;
|
|
}
|
|
|
|
// Call RequestEquipItemAsync with inventory slot and equip location
|
|
UnityGameSession.RequestEquipItemAsync((byte)currentSelectedSlot, equipLocation, () =>
|
|
{
|
|
Debug.Log($"[InventoryUI] Equip request sent for item {currentSelectedItem.m_tid} from slot {currentSelectedSlot} to equip location {equipLocation}");
|
|
// Refresh inventory after equip
|
|
RefreshAll();
|
|
});
|
|
}
|
|
|
|
private void UnequipItem()
|
|
{
|
|
if (currentSelectedItem == null) return;
|
|
|
|
// Find empty slot in PACK_INVENTORY
|
|
int emptySlot = FindEmptyInventorySlot();
|
|
if (emptySlot == -1)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] No empty slots available in inventory for unequipping");
|
|
return;
|
|
}
|
|
|
|
// For unequipping, the equip location is the current equipment slot
|
|
// We can use the slot number as the equip location
|
|
byte equipLocation = (byte)currentSelectedSlot;
|
|
|
|
// Call RequestEquipItemAsync with empty inventory slot and current equip location
|
|
UnityGameSession.RequestEquipItemAsync((byte)emptySlot, equipLocation, () =>
|
|
{
|
|
Debug.Log($"[InventoryUI] Unequip request sent for item {currentSelectedItem.m_tid} from equip location {equipLocation} to inventory slot {emptySlot}");
|
|
// Refresh inventory after unequip
|
|
RefreshAll();
|
|
});
|
|
}
|
|
|
|
private void DropInventoryItem()
|
|
{
|
|
if (currentSelectedItem == null) return;
|
|
|
|
// Call RequestDropIvrtItem with slot index and amount
|
|
UnityGameSession.RequestDropIvrtItem((byte)currentSelectedSlot, 1);
|
|
Debug.Log($"[InventoryUI] Drop request sent for inventory item {currentSelectedItem.m_tid} from slot {currentSelectedSlot} with amount {currentSelectedItem.m_iCount}");
|
|
|
|
// Refresh inventory after drop
|
|
RefreshAll();
|
|
}
|
|
|
|
private void DropEquipItem()
|
|
{
|
|
if (currentSelectedItem == null) return;
|
|
|
|
// Call RequestDropEquipItem with slot index
|
|
UnityGameSession.RequestDropEquipItem((byte)currentSelectedSlot);
|
|
Debug.Log($"[InventoryUI] Drop request sent for equipment item {currentSelectedItem.m_tid} from slot {currentSelectedSlot}");
|
|
|
|
// Refresh inventory after drop
|
|
RefreshAll();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Separate part of the currently selected stack into a new empty slot (C2S MOVE_IVTR_ITEM with partial count).
|
|
/// Your UI can call this directly once you've picked the split amount.
|
|
/// </summary>
|
|
public bool SeparateSelectedStack(int splitAmount, int preferredEmptySlot = -1)
|
|
{
|
|
if (currentSelectedItem == null)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] SeparateSelectedStack: no item selected");
|
|
return false;
|
|
}
|
|
|
|
// This client-side move command is for the main inventory pack (IVTRTYPE_PACK) like the original client.
|
|
// Task pack / other packs may need different commands depending on server implementation.
|
|
if (currentSelectedPackage != PKG_INVENTORY)
|
|
{
|
|
Debug.LogWarning($"[InventoryUI] SeparateSelectedStack: unsupported package {currentSelectedPackage} (only PKG_INVENTORY supported)");
|
|
return false;
|
|
}
|
|
|
|
int total = currentSelectedItem.m_iCount;
|
|
if (total <= 1)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] SeparateSelectedStack: item count <= 1");
|
|
return false;
|
|
}
|
|
|
|
if (splitAmount <= 0 || splitAmount >= total)
|
|
{
|
|
Debug.LogWarning($"[InventoryUI] SeparateSelectedStack: invalid splitAmount={splitAmount}, total={total}");
|
|
return false;
|
|
}
|
|
|
|
int emptySlot = preferredEmptySlot >= 0 ? preferredEmptySlot : FindEmptySlotInPackage(PKG_INVENTORY);
|
|
if (emptySlot < 0)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] SeparateSelectedStack: no empty slot available");
|
|
return false;
|
|
}
|
|
|
|
// Send MOVE_IVTR_ITEM(src, dest, count). When dest is empty and count < stack, server will split.
|
|
UnityGameSession.RequestMoveIvtrItem((byte)currentSelectedSlot, (byte)emptySlot, (uint)splitAmount);
|
|
|
|
// UI will update when S2C item operation arrives; this is a best-effort immediate refresh for responsiveness.
|
|
RefreshAll();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merge from the selected slot into another stack of the same template (C++ <c>CDlgInventory::ExchangeItem</c>
|
|
/// branch that calls <c>c2s_CmdMoveIvtrItem</c> when dest matches and pile limit allows).
|
|
/// Picks the lowest-index compatible destination with free stack space.
|
|
/// </summary>
|
|
public bool CombineSelectedStack()
|
|
{
|
|
if (currentSelectedItem == null)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] CombineSelectedStack: no item selected");
|
|
return false;
|
|
}
|
|
|
|
if (currentSelectedPackage != PKG_INVENTORY)
|
|
{
|
|
Debug.LogWarning($"[InventoryUI] CombineSelectedStack: unsupported package {currentSelectedPackage} (only PKG_INVENTORY supported)");
|
|
return false;
|
|
}
|
|
|
|
if (!TryGetInventoryMergeTarget(currentSelectedSlot, currentSelectedItem, out int dstSlot, out int moveAmount))
|
|
{
|
|
Debug.LogWarning("[InventoryUI] CombineSelectedStack: no merge target with free stack space");
|
|
return false;
|
|
}
|
|
|
|
UnityGameSession.RequestMoveIvtrItem((byte)currentSelectedSlot, (byte)dstSlot, (uint)moveAmount);
|
|
RefreshAll();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pile cap for stacking: instance <see cref="EC_IvtrItem.m_iPileLimit"/> (from essence <c>pile_num_max</c>) is authoritative;
|
|
/// <see cref="EC_IvtrItem.GetPileLimit"/> may still be 1 if element reflection misses field names.
|
|
/// </summary>
|
|
private static int GetEffectivePileLimitForStack(int tid, EC_IvtrItem a, EC_IvtrItem b)
|
|
{
|
|
int p = Math.Max(1, EC_IvtrItem.GetPileLimit(tid));
|
|
if (a != null)
|
|
p = Math.Max(p, Math.Max(1, a.GetPileLimitInstance()));
|
|
if (b != null)
|
|
p = Math.Max(p, Math.Max(1, b.GetPileLimitInstance()));
|
|
return p;
|
|
}
|
|
|
|
/// <summary>First eligible slot (lowest index) that can accept part or all of <paramref name="srcItem"/>.</summary>
|
|
private static bool TryGetInventoryMergeTarget(int srcSlot, EC_IvtrItem srcItem, out int dstSlot, out int moveAmount)
|
|
{
|
|
dstSlot = -1;
|
|
moveAmount = 0;
|
|
if (srcItem == null || srcItem.IsFrozen())
|
|
return false;
|
|
|
|
int tid = srcItem.GetTemplateID();
|
|
int srcCount = srcItem.GetCount();
|
|
if (srcCount < 1)
|
|
return false;
|
|
|
|
var host = CECGameRun.Instance?.GetHostPlayer();
|
|
var inv = host?.GetInventory(PKG_INVENTORY);
|
|
if (inv == null)
|
|
return false;
|
|
|
|
int size = inv.GetSize();
|
|
for (int i = 0; i < size; i++)
|
|
{
|
|
if (i == srcSlot)
|
|
continue;
|
|
|
|
var dst = inv.GetItem(i, false);
|
|
if (dst == null || dst.IsFrozen())
|
|
continue;
|
|
if (dst.GetTemplateID() != tid)
|
|
continue;
|
|
|
|
int pile = GetEffectivePileLimitForStack(tid, srcItem, dst);
|
|
if (pile <= 1)
|
|
continue;
|
|
|
|
int room = pile - dst.GetCount();
|
|
if (room <= 0)
|
|
continue;
|
|
|
|
int move = Math.Min(srcCount, room);
|
|
if (move <= 0)
|
|
continue;
|
|
|
|
dstSlot = i;
|
|
moveAmount = move;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private int FindEmptySlotInPackage(byte package)
|
|
{
|
|
var host = CECGameRun.Instance?.GetHostPlayer();
|
|
var inv = host?.GetInventory(package);
|
|
if (inv == null) return -1;
|
|
|
|
int size = inv.GetSize();
|
|
for (int i = 0; i < size; i++)
|
|
{
|
|
if (inv.GetItem(i, false) == null)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
private int FindEmptyInventorySlot()
|
|
{
|
|
var inventoryData = model.GetInventoryData(PKG_INVENTORY);
|
|
if (inventoryData == null) return -1;
|
|
|
|
// Find first empty slot (assuming slots are numbered 0, 1, 2, ...)
|
|
for (int i = 0; i < 100; i++) // Assuming max 100 inventory slots
|
|
{
|
|
if (!inventoryData.ContainsKey(i))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get item description from string table
|
|
/// </summary>
|
|
/// <param name="templateId">Item template ID</param>
|
|
/// <returns>Item description text or null if not found</returns>
|
|
private string GetItemDescription(int templateId)
|
|
{
|
|
try
|
|
{
|
|
// Prefer mapped message id if available
|
|
if (EC_Game.TryGetItemMsg(templateId, out int messageId, out int displayMode))
|
|
{
|
|
var itemDesc = EC_Game.GetItemDesc();
|
|
if (itemDesc != null && itemDesc.IsInitialized())
|
|
{
|
|
string description = itemDesc.GetWideString(messageId);
|
|
if (!string.IsNullOrEmpty(description))
|
|
{
|
|
return description;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: try direct template id
|
|
{
|
|
var itemDesc = EC_Game.GetItemDesc();
|
|
if (itemDesc != null && itemDesc.IsInitialized())
|
|
{
|
|
string description = itemDesc.GetWideString(templateId);
|
|
if (!string.IsNullOrEmpty(description))
|
|
return description;
|
|
}
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogWarning($"[InventoryUI] Error getting item description for ID {templateId}: {ex.Message}");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get extended item description from string table
|
|
/// </summary>
|
|
/// <param name="templateId">Item template ID</param>
|
|
/// <returns>Extended item description text or null if not found</returns>
|
|
private string GetItemExtendedDescription(int templateId)
|
|
{
|
|
try
|
|
{
|
|
// Prefer mapped message id if available
|
|
if (EC_Game.TryGetItemMsg(templateId, out int messageId, out int displayMode))
|
|
{
|
|
var itemExtDesc = EC_Game.GetItemExtDesc();
|
|
if (itemExtDesc != null && itemExtDesc.IsInitialized())
|
|
{
|
|
string extendedDesc = itemExtDesc.GetWideString(messageId);
|
|
if (!string.IsNullOrEmpty(extendedDesc))
|
|
{
|
|
return extendedDesc;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: direct template id
|
|
{
|
|
var itemExtDesc = EC_Game.GetItemExtDesc();
|
|
if (itemExtDesc != null && itemExtDesc.IsInitialized())
|
|
{
|
|
string extendedDesc = itemExtDesc.GetWideString(templateId);
|
|
if (!string.IsNullOrEmpty(extendedDesc))
|
|
return extendedDesc;
|
|
}
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogWarning($"[InventoryUI] Error getting extended item description for ID {templateId}: {ex.Message}");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get user-friendly text for item state
|
|
/// </summary>
|
|
/// <param name="state">Item state value</param>
|
|
/// <returns>Human-readable state text</returns>
|
|
private string GetItemStateText(int state)
|
|
{
|
|
switch (state)
|
|
{
|
|
case 0: return "Normal";
|
|
case 1: return "Equipped";
|
|
case 2: return "Broken";
|
|
case 3: return "Locked";
|
|
default: return $"State: {state}";
|
|
}
|
|
}
|
|
|
|
// Equipment indices and resolution helpers moved to EC_IvtrType
|
|
|
|
// === MVC: Model ===
|
|
private class InventoryModel
|
|
{
|
|
public Dictionary<int, EC_IvtrItem> GetInventoryData(byte package)
|
|
{
|
|
// Read from host player's per-package CECInventory instance
|
|
var host = CECGameRun.Instance?.GetHostPlayer();
|
|
var inv = host?.GetInventory(package);
|
|
var result = new Dictionary<int, EC_IvtrItem>();
|
|
if (inv == null)
|
|
return result;
|
|
|
|
int size = inv.GetSize();
|
|
for (int i = 0; i < size; i++)
|
|
{
|
|
var item = inv.GetItem(i, false);
|
|
if (item != null)
|
|
result[i] = item;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// === MVC: View ===
|
|
private class InventoryView
|
|
{
|
|
private static readonly Dictionary<Image, Sprite> _defaultSprites = new Dictionary<Image, Sprite>();
|
|
|
|
private static readonly Dictionary<Button, Image> _overlayImages = new Dictionary<Button, Image>();
|
|
|
|
public void RenderPackage(List<Button> buttons, Dictionary<int, EC_IvtrItem> items, byte package, System.Action<byte, int> onClick, System.Func<int, EC_IvtrItem, string> getDisplayText)
|
|
{
|
|
if (buttons == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int slot = 0; slot < buttons.Count; slot++)
|
|
{
|
|
var button = buttons[slot];
|
|
if (button == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
EC_IvtrItem itemData = null;
|
|
bool hasItem = items != null && items.TryGetValue(slot, out itemData);
|
|
|
|
button.onClick.RemoveAllListeners();
|
|
int capturedSlot = slot;
|
|
button.onClick.AddListener(() => onClick(package, capturedSlot));
|
|
|
|
// Update icon and count
|
|
var image = button.GetComponent<Image>();
|
|
if (image != null)
|
|
{
|
|
// Store default sprite
|
|
if (!_defaultSprites.ContainsKey(image))
|
|
{
|
|
_defaultSprites[image] = image.sprite;
|
|
}
|
|
|
|
// Set icon sprite based on item
|
|
if (hasItem && itemData != null && itemData.m_iCount > 0)
|
|
{
|
|
var sprite = EC_IvtrItemUtils.Instance.ResolveItemIconSprite(itemData.m_tid);
|
|
image.sprite = sprite;
|
|
image.enabled = true;
|
|
|
|
UpdateItemCountText(button, itemData.m_iCount);
|
|
}
|
|
else
|
|
{
|
|
// Restore default sprite
|
|
image.sprite = _defaultSprites[image];
|
|
image.enabled = true;
|
|
|
|
UpdateItemCountText(button, 0);
|
|
}
|
|
}
|
|
|
|
// Setup drag-drop events
|
|
var eventTrigger = button.GetComponent<EventTrigger>();
|
|
if (eventTrigger == null)
|
|
eventTrigger = button.gameObject.AddComponent<EventTrigger>();
|
|
|
|
eventTrigger.triggers.RemoveAll(e =>
|
|
e.eventID == EventTriggerType.BeginDrag ||
|
|
e.eventID == EventTriggerType.Drag ||
|
|
e.eventID == EventTriggerType.EndDrag ||
|
|
e.eventID == EventTriggerType.Drop);
|
|
|
|
void AddEvent(EventTriggerType type, UnityEngine.Events.UnityAction<BaseEventData> action)
|
|
{
|
|
var entry = new EventTrigger.Entry { eventID = type };
|
|
entry.callback.AddListener(action);
|
|
eventTrigger.triggers.Add(entry);
|
|
}
|
|
|
|
//AddEvent(EventTriggerType.BeginDrag, (data) => ((EC_InventoryUI)button.GetComponentInParent<EC_InventoryUI>()).OnBeginDrag((PointerEventData)data));
|
|
//AddEvent(EventTriggerType.Drag, (data) => ((EC_InventoryUI)button.GetComponentInParent<EC_InventoryUI>()).OnDrag((PointerEventData)data));
|
|
//AddEvent(EventTriggerType.EndDrag, (data) => ((EC_InventoryUI)button.GetComponentInParent<EC_InventoryUI>()).OnEndDrag((PointerEventData)data));
|
|
//AddEvent(EventTriggerType.Drop, (data) => ((EC_InventoryUI)button.GetComponentInParent<EC_InventoryUI>()).OnDrop((PointerEventData)data));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update cooldown overlay for item
|
|
/// 更新物品冷却遮罩
|
|
/// </summary>
|
|
public void UpdateCooldownOverlay(Button button, EC_IvtrItem itemData)
|
|
{
|
|
if (button == null || itemData == null)
|
|
{
|
|
Debug.LogWarning("[UpdateCooldownOverlay] Button or itemData is null");
|
|
return;
|
|
}
|
|
|
|
// Find or cache overlay image
|
|
// 查找或缓存遮罩图片
|
|
Image overlay = null;
|
|
if (_overlayImages.TryGetValue(button, out overlay))
|
|
{
|
|
if (overlay == null)
|
|
{
|
|
// Cached but destroyed, remove and search again
|
|
// 已缓存但被销毁,移除并重新查找
|
|
Debug.LogWarning($"[UpdateCooldownOverlay] Cached overlay was destroyed for button {button.name}");
|
|
_overlayImages.Remove(button);
|
|
}
|
|
}
|
|
|
|
if (overlay == null)
|
|
{
|
|
// Find image_overlay in button's children
|
|
// 在按钮子物体中查找 image_overlay
|
|
var overlayTransform = button.transform.Find("image_overlay");
|
|
if (overlayTransform != null)
|
|
{
|
|
overlay = overlayTransform.GetComponent<Image>();
|
|
if (overlay != null)
|
|
{
|
|
_overlayImages[button] = overlay;
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[UpdateCooldownOverlay] Found transform but no Image component on image_overlay for button {button.name}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[UpdateCooldownOverlay] Cannot find image_overlay child in button {button.name}");
|
|
}
|
|
}
|
|
|
|
if (overlay == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get cooldown info from item
|
|
// 从物品获取冷却信息
|
|
int maxCooldown = -1;
|
|
int currentCooldown = itemData.GetCoolTime(out maxCooldown);
|
|
|
|
if (currentCooldown > 0)
|
|
{
|
|
// Show overlay and set fill amount
|
|
// 显示遮罩并设置填充量
|
|
overlay.gameObject.SetActive(true);
|
|
|
|
// Calculate fill amount (1 = full cooldown, 0 = ready)
|
|
// 计算填充量(1=完全冷却,0=就绪)
|
|
if (maxCooldown > 0)
|
|
{
|
|
float fillAmount = (float)currentCooldown / maxCooldown;
|
|
overlay.fillAmount = fillAmount;
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
// Hide overlay when not in cooldown
|
|
// 不在冷却时隐藏遮罩
|
|
overlay.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hide cooldown overlay
|
|
/// 隐藏冷却遮罩
|
|
/// </summary>
|
|
public void HideCooldownOverlay(Button button)
|
|
{
|
|
if (button == null)
|
|
return;
|
|
|
|
Image overlay = null;
|
|
if (_overlayImages.TryGetValue(button, out overlay) && overlay != null)
|
|
{
|
|
overlay.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
/// <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>
|
|
/// <param name="button">The inventory button</param>
|
|
/// <param name="count">Item count (0 to hide)</param>
|
|
private void UpdateItemCountText(Button button, int count)
|
|
{
|
|
if (button == null) return;
|
|
|
|
TMPro.TextMeshProUGUI tmpText = null;
|
|
Text legacyText = null;
|
|
|
|
var textTransform = FindStackCountTextTransform(button.transform);
|
|
|
|
if (textTransform != null)
|
|
{
|
|
tmpText = textTransform.GetComponent<TMPro.TextMeshProUGUI>();
|
|
legacyText = textTransform.GetComponent<Text>();
|
|
}
|
|
|
|
if (tmpText == null && legacyText == null)
|
|
{
|
|
tmpText = button.GetComponentInChildren<TMPro.TextMeshProUGUI>();
|
|
if (tmpText == null)
|
|
legacyText = button.GetComponentInChildren<Text>();
|
|
}
|
|
|
|
// Update text
|
|
if (count > 1)
|
|
{
|
|
string countText = count.ToString();
|
|
|
|
if (tmpText != null)
|
|
{
|
|
tmpText.text = countText;
|
|
tmpText.gameObject.SetActive(true);
|
|
}
|
|
else if (legacyText != null)
|
|
{
|
|
legacyText.text = countText;
|
|
legacyText.gameObject.SetActive(true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Hide when count <= 1
|
|
if (tmpText != null)
|
|
{
|
|
tmpText.text = "";
|
|
tmpText.gameObject.SetActive(false);
|
|
}
|
|
else if (legacyText != null)
|
|
{
|
|
legacyText.text = "";
|
|
legacyText.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// === Detail Panel Helpers ===
|
|
// Note: TextOutlet has been moved to EC_UIUtility for reuse across the codebase
|
|
|
|
private void ShowDetailPanel(bool show)
|
|
{
|
|
EC_UIUtility.ShowPanel(detailPanelRoot.gameObject, show);
|
|
if (!show)
|
|
{
|
|
RefreshSplitControlsVisibility(0, null);
|
|
RefreshCombineControlsVisibility(0, null);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Split affordance only for main bag stacks (same as <see cref="SeparateSelectedStack"/>).
|
|
/// If <c>splitCloseButton</c> is parented under <c>splitPanelRoot</c>, its visibility follows the modal (not this rule).
|
|
/// </summary>
|
|
private bool IsSplitCloseButtonOnModal()
|
|
{
|
|
return splitPanelRoot != null && splitCloseButton != null &&
|
|
splitCloseButton.transform.IsChildOf(splitPanelRoot.transform);
|
|
}
|
|
|
|
private bool CanShowSplitControls(byte package, EC_IvtrItem item)
|
|
{
|
|
if (item == null)
|
|
return false;
|
|
if (package != PKG_INVENTORY)
|
|
return false;
|
|
int n = item.GetCount();
|
|
if (n < 2 && item.m_iCount >= 2)
|
|
n = item.m_iCount;
|
|
return n >= 2;
|
|
}
|
|
|
|
/// <summary>
|
|
/// <c>splitOpenButton</c>: show only when a splittable main-bag stack is selected (detail panel).
|
|
/// <c>splitCloseButton</c>: if it lives on the split modal, show only when modal is open; else same as open (detail-placed cancel).
|
|
/// </summary>
|
|
private void RefreshSplitControlsVisibility(byte package, EC_IvtrItem item)
|
|
{
|
|
bool canSplit = CanShowSplitControls(package, item);
|
|
|
|
if (splitOpenButton != null)
|
|
splitOpenButton.gameObject.SetActive(canSplit);
|
|
|
|
if (splitCloseButton != null)
|
|
{
|
|
if (IsSplitCloseButtonOnModal())
|
|
splitCloseButton.gameObject.SetActive(splitPanelRoot != null && splitPanelRoot.activeSelf);
|
|
else
|
|
splitCloseButton.gameObject.SetActive(canSplit);
|
|
}
|
|
}
|
|
|
|
private bool CanShowCombineControls(byte package, EC_IvtrItem item)
|
|
{
|
|
if (item == null || package != PKG_INVENTORY)
|
|
return false;
|
|
return TryGetInventoryMergeTarget(currentSelectedSlot, item, out _, out _);
|
|
}
|
|
|
|
private void RefreshCombineControlsVisibility(byte package, EC_IvtrItem item)
|
|
{
|
|
bool canCombine = CanShowCombineControls(package, item);
|
|
if (combineStackButton != null)
|
|
combineStackButton.gameObject.SetActive(canCombine);
|
|
}
|
|
|
|
private Button GetButtonForSlot(byte package, int slot)
|
|
{
|
|
List<Button> list = null;
|
|
switch (package)
|
|
{
|
|
case PKG_INVENTORY:
|
|
case PKG_TASK:
|
|
list = inventoryPackButtons;
|
|
break;
|
|
case PKG_EQUIPMENT:
|
|
list = equipmentPackButtons;
|
|
break;
|
|
case PKG_FASHION:
|
|
list = fashionPackButtons;
|
|
break;
|
|
}
|
|
if (list == null || slot < 0 || slot >= list.Count)
|
|
{
|
|
return null;
|
|
}
|
|
return list[slot];
|
|
}
|
|
|
|
private void PositionDetailPanelNearButton(byte package, int slot)
|
|
{
|
|
if (detailPanelRoot == null)
|
|
{
|
|
return;
|
|
}
|
|
var panelRect = detailPanelRoot.transform as RectTransform;
|
|
if (panelRect == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var button = GetButtonForSlot(package, slot);
|
|
if (button == null)
|
|
{
|
|
return;
|
|
}
|
|
var buttonRect = button.transform as RectTransform;
|
|
if (buttonRect == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Use the extracted utility function for positioning
|
|
EC_UIUtility.PositionPanelNearButton(panelRect, buttonRect, detailPanelOffset);
|
|
}
|
|
|
|
private void FillDetailPanel(byte package, EC_IvtrItem item)
|
|
{
|
|
if (item == null)
|
|
{
|
|
ShowDetailPanel(false);
|
|
return;
|
|
}
|
|
|
|
// Get user-friendly name
|
|
string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(item.m_tid);
|
|
// Centralised description:
|
|
// - For equipment, prefer EC_IvtrEquip description (includes stats, addons, sockets, etc.)
|
|
// - For other items, use EC_IvtrItem.GetDesc which reads from string tables.
|
|
string fullDesc = null;
|
|
if (showEquipmentDetails && currentSelectedEquipment != null)
|
|
{
|
|
fullDesc = currentSelectedEquipment.GetDesc(EC_IvtrItem.DescType.DESC_NORMAL, EC_Game.GetGameRun().GetHostPlayer().GetEquipment());
|
|
}
|
|
else
|
|
{
|
|
fullDesc = item.GetDesc(EC_IvtrItem.DescType.DESC_NORMAL, EC_Game.GetGameRun().GetHostPlayer().GetEquipment());
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(fullDesc))
|
|
{
|
|
// Replace C++ style "\r" line separators with real newlines for TMP
|
|
fullDesc = fullDesc.Replace("\\r", "\n");
|
|
descriptionText?.Set(fullDesc);
|
|
}
|
|
else
|
|
{
|
|
// Fallback to legacy string-table description if centralised pipeline returns empty
|
|
string itemDescription = GetItemDescription(item.m_tid);
|
|
descriptionText?.Set(itemDescription ?? "No description available");
|
|
}
|
|
|
|
// Get extended description exactly like C++ code: g_pGame->GetItemExtDesc(m_tid)
|
|
// C++ code doesn't check IsInitialized() - it just calls GetWideString() directly
|
|
// 完全按照C++代码获取扩展描述:g_pGame->GetItemExtDesc(m_tid)
|
|
// C++代码不检查IsInitialized() - 它直接调用GetWideString()
|
|
|
|
// Setup equip and drop buttons
|
|
SetupEquipButton(package, item);
|
|
SetupDropButton(package, item);
|
|
|
|
// Show panel first
|
|
// 先显示面板
|
|
ShowDetailPanel(true);
|
|
// After detail root is active so child buttons actually appear (fixes split controls under inactive parents).
|
|
RefreshSplitControlsVisibility(package, item);
|
|
RefreshCombineControlsVisibility(package, item);
|
|
//Refresh the position of the description text. Used for UI logic purpose.
|
|
descriptionText.tmp.gameObject.GetComponent<ItemInfoText>()?.RefreshLayout();
|
|
|
|
}
|
|
|
|
private void SetupEquipButton(byte package, EC_IvtrItem item)
|
|
{
|
|
if (equipButton == null) return;
|
|
|
|
// Clear previous listeners
|
|
equipButton.onClick.RemoveAllListeners();
|
|
equipButton.onClick.AddListener(OnEquipButtonClicked);
|
|
|
|
// Set button text and visibility based on package
|
|
var buttonText = equipButton.GetComponentInChildren<UnityEngine.UI.Text>();
|
|
if (buttonText == null)
|
|
{
|
|
var tmpText = equipButton.GetComponentInChildren<TMPro.TextMeshProUGUI>();
|
|
if (tmpText != null)
|
|
{
|
|
if (package == PKG_INVENTORY || package == PKG_TASK)
|
|
{
|
|
//if item is @EC_IvtrEquip and is not equipped, show equip button
|
|
if(item is EC_IvtrEquip)
|
|
{
|
|
tmpText.text = "Trang bị";
|
|
equipButton.gameObject.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
tmpText.text = "Sử dụng";
|
|
equipButton.gameObject.SetActive(true);
|
|
}
|
|
}
|
|
else if (package == PKG_EQUIPMENT)
|
|
{
|
|
tmpText.text = "Tháo";
|
|
equipButton.gameObject.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
equipButton.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (package == PKG_INVENTORY || package == PKG_TASK)
|
|
{
|
|
if(item is EC_IvtrEquip)
|
|
{
|
|
buttonText.text = "Trang bị";
|
|
equipButton.gameObject.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
buttonText.text = "Sử dụng";
|
|
equipButton.gameObject.SetActive(true);
|
|
}
|
|
}
|
|
else if (package == PKG_EQUIPMENT)
|
|
{
|
|
buttonText.text = "Tháo";
|
|
equipButton.gameObject.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
equipButton.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SetupDropButton(byte package, EC_IvtrItem item)
|
|
{
|
|
if (dropButton == null) return;
|
|
|
|
// Clear previous listeners
|
|
dropButton.onClick.RemoveAllListeners();
|
|
dropButton.onClick.AddListener(OnDropButtonClicked);
|
|
|
|
// Set button text and visibility based on package
|
|
var buttonText = dropButton.GetComponentInChildren<UnityEngine.UI.Text>();
|
|
if (buttonText == null)
|
|
{
|
|
var tmpText = dropButton.GetComponentInChildren<TMPro.TextMeshProUGUI>();
|
|
if (tmpText != null)
|
|
{
|
|
if (package == PKG_INVENTORY || package == PKG_EQUIPMENT)
|
|
{
|
|
tmpText.text = "Vứt";
|
|
dropButton.gameObject.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
dropButton.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (package == PKG_INVENTORY || package == PKG_EQUIPMENT)
|
|
{
|
|
buttonText.text = "Vứt";
|
|
dropButton.gameObject.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
dropButton.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleDetailPanelDismissInput()
|
|
{
|
|
if (detailPanelRoot == null || !detailPanelRoot.gameObject.activeSelf)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryGetPointerDownPosition(out Vector2 screenPosition))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (IsPointerOverDetailPanel(screenPosition))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ShowDetailPanel(false);
|
|
}
|
|
|
|
private bool TryGetPointerDownPosition(out Vector2 screenPosition)
|
|
{
|
|
if (Input.GetMouseButtonDown(0))
|
|
{
|
|
screenPosition = Input.mousePosition;
|
|
return true;
|
|
}
|
|
|
|
if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)
|
|
{
|
|
screenPosition = Input.GetTouch(0).position;
|
|
return true;
|
|
}
|
|
|
|
screenPosition = default;
|
|
return false;
|
|
}
|
|
|
|
private bool IsPointerOverDetailPanel(Vector2 screenPosition)
|
|
{
|
|
var panelRect = detailPanelRoot.transform as RectTransform;
|
|
if (panelRect == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var eventSystem = EventSystem.current;
|
|
if (eventSystem != null)
|
|
{
|
|
var pointerData = new PointerEventData(eventSystem)
|
|
{
|
|
position = screenPosition
|
|
};
|
|
|
|
var raycastResults = new List<RaycastResult>();
|
|
eventSystem.RaycastAll(pointerData, raycastResults);
|
|
|
|
for (int i = 0; i < raycastResults.Count; i++)
|
|
{
|
|
var hitTransform = raycastResults[i].gameObject != null ? raycastResults[i].gameObject.transform : null;
|
|
if (hitTransform != null && (hitTransform == panelRect.transform || hitTransform.IsChildOf(panelRect.transform)))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
var canvas = detailPanelRoot.GetComponentInParent<Canvas>();
|
|
Camera uiCamera = null;
|
|
if (canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay)
|
|
{
|
|
uiCamera = canvas.worldCamera;
|
|
}
|
|
|
|
return RectTransformUtility.RectangleContainsScreenPoint(panelRect, screenPosition, uiCamera);
|
|
}
|
|
//public void OnBeginDrag(PointerEventData eventData)
|
|
//{
|
|
// var btn = eventData.pointerDrag?.GetComponent<Button>();
|
|
// if (btn != null)
|
|
// {
|
|
// for (int i = 0; i < inventoryPackButtons.Count; i++)
|
|
// {
|
|
// if (btn == inventoryPackButtons[i])
|
|
// {
|
|
// var invItems = model.GetInventoryData(PKG_INVENTORY);
|
|
// if(invItems == null || !invItems.ContainsKey(i))
|
|
// {
|
|
// return;
|
|
// }
|
|
|
|
// draggedItemSourceSlot = i;
|
|
// draggedItemSourcePackage = PKG_INVENTORY;
|
|
|
|
// var img = btn.GetComponent<Image>();
|
|
// if (img != null && currentDragImage != null)
|
|
// {
|
|
// currentDragImage.sprite = img.sprite;
|
|
// currentDragImage.SetNativeSize();
|
|
// currentDragImage.color = img.color;
|
|
// currentDragImage.gameObject.SetActive(true);
|
|
// isDragging = true;
|
|
// }
|
|
// break;
|
|
// }
|
|
// }
|
|
// for (int i = 0; i < equipmentPackButtons.Count; i++)
|
|
// {
|
|
// if (btn == equipmentPackButtons[i])
|
|
// {
|
|
// var equipItems = model.GetInventoryData(PKG_EQUIPMENT);
|
|
// if(equipItems == null || !equipItems.ContainsKey(i))
|
|
// {
|
|
// return;
|
|
// }
|
|
|
|
// draggedItemSourceSlot = i;
|
|
// draggedItemSourcePackage = PKG_EQUIPMENT;
|
|
// var img = btn.GetComponent<Image>();
|
|
// if (img != null && currentDragImage != null)
|
|
// {
|
|
// currentDragImage.sprite = img.sprite;
|
|
// currentDragImage.SetNativeSize();
|
|
// currentDragImage.color = img.color;
|
|
// currentDragImage.gameObject.SetActive(true);
|
|
// isDragging = true;
|
|
// }
|
|
// return;
|
|
// }
|
|
// }
|
|
// }
|
|
//}
|
|
|
|
//public void OnDrag(PointerEventData eventData)
|
|
//{
|
|
// if(isDragging && currentDragImage != null)
|
|
// {
|
|
// currentDragImage.transform.position = eventData.position;
|
|
// }
|
|
//}
|
|
|
|
//public void OnEndDrag(PointerEventData eventData)
|
|
//{
|
|
// draggedItemSourceSlot = -1;
|
|
// draggedItemSourcePackage = 0;
|
|
// if(currentDragImage != null)
|
|
// {
|
|
// currentDragImage.gameObject.SetActive(false);
|
|
// isDragging = false;
|
|
// }
|
|
//}
|
|
|
|
//public void OnDrop(PointerEventData eventData)
|
|
//{
|
|
// var btn = eventData.pointerCurrentRaycast.gameObject?.GetComponent<Button>();
|
|
// if (btn != null && draggedItemSourcePackage == PKG_INVENTORY)
|
|
// {
|
|
// for (int i = 0; i < equipmentPackButtons.Count; i++)
|
|
// {
|
|
// if (btn == equipmentPackButtons[i])
|
|
// {
|
|
// UnityGameSession.RequestEquipItemAsync((byte)draggedItemSourceSlot, (byte)i, () =>
|
|
// {
|
|
// Debug.Log($"[InventoryUI] Drag-drop equip: từ inventory slot {draggedItemSourceSlot} sang equipment slot {i}");
|
|
// RefreshAll();
|
|
// });
|
|
// break;
|
|
// }
|
|
// }
|
|
// }
|
|
// else if(btn != null && draggedItemSourcePackage == PKG_EQUIPMENT)
|
|
// {
|
|
// for (int i = 0; i < inventoryPackButtons.Count; i++)
|
|
// {
|
|
// if (btn == inventoryPackButtons[i])
|
|
// {
|
|
// UnityGameSession.RequestEquipItemAsync((byte)i, (byte)draggedItemSourceSlot, () =>
|
|
// {
|
|
// Debug.Log($"[InventoryUI] Drag-drop unequip: từ equipment slot {draggedItemSourceSlot} sang inventory slot {i}");
|
|
// RefreshAll();
|
|
// });
|
|
// break;
|
|
// }
|
|
// }
|
|
// }
|
|
// draggedItemSourceSlot = -1;
|
|
// draggedItemSourcePackage = 0;
|
|
//}
|
|
|
|
}
|
|
}
|