Files
test/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs
T

1409 lines
53 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("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();
//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);
}
// 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);
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()
{
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();
}
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);
}
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);
//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 = "Equip";
equipButton.gameObject.SetActive(true);
}
else
{
tmpText.text = "Use";
equipButton.gameObject.SetActive(true);
}
}
else if (package == PKG_EQUIPMENT)
{
tmpText.text = "UnEquip";
equipButton.gameObject.SetActive(true);
}
else
{
equipButton.gameObject.SetActive(false);
}
}
}
else
{
if (package == PKG_INVENTORY || package == PKG_TASK)
{
if(item is EC_IvtrEquip)
{
buttonText.text = "Equip";
equipButton.gameObject.SetActive(true);
}
else
{
buttonText.text = "Use";
equipButton.gameObject.SetActive(true);
}
}
else if (package == PKG_EQUIPMENT)
{
buttonText.text = "UnEquip";
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 = "Drop";
dropButton.gameObject.SetActive(true);
}
else
{
dropButton.gameObject.SetActive(false);
}
}
}
else
{
if (package == PKG_INVENTORY || package == PKG_EQUIPMENT)
{
buttonText.text = "Drop";
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;
//}
}
}