1265 lines
44 KiB
C#
1265 lines
44 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using BrewMonster.Network;
|
|
using BrewMonster;
|
|
using BrewMonster.Common;
|
|
using ModelRenderer.Scripts.GameData;
|
|
using PerfectWorld.Scripts.Managers;
|
|
|
|
namespace BrewMonster.Scripts.Managers
|
|
{
|
|
public class EC_InventoryUI : MonoBehaviour
|
|
{
|
|
[Header("Pack Buttons (assign in Inspector)")]
|
|
[SerializeField] private List<Button> inventoryPackButtons = new List<Button>(); // byPackage: 0
|
|
[SerializeField] private List<Button> equipmentPackButtons = new List<Button>(); // byPackage: 1
|
|
[SerializeField] private List<Button> fashionPackButtons = new List<Button>(); // byPackage: 3
|
|
|
|
[Header("Detail Panel (assign in Inspector)")]
|
|
[SerializeField] private GameObject detailPanelRoot;
|
|
[SerializeField] private Vector2 detailPanelOffset = new Vector2(20f, 0f);
|
|
[SerializeField] private bool hideDetailOnStart = true;
|
|
[SerializeField] private TextOutlet nameText;
|
|
[SerializeField] private TextOutlet descriptionText;
|
|
[SerializeField] private TextOutlet extendedDescText;
|
|
[SerializeField] private TextOutlet countText;
|
|
[SerializeField] private TextOutlet stateText;
|
|
[SerializeField] private TextOutlet expireText;
|
|
[SerializeField] private Button equipButton;
|
|
[SerializeField] private Button dropButton;
|
|
|
|
[Header("Equipment Details (assign in Inspector)")]
|
|
[SerializeField] private TextOutlet levelReqText;
|
|
[SerializeField] private TextOutlet statsReqText;
|
|
[SerializeField] private TextOutlet enduranceText;
|
|
[SerializeField] private TextOutlet repairCostText;
|
|
[SerializeField] private TextOutlet propertiesText;
|
|
[SerializeField] private TextOutlet refineText;
|
|
[SerializeField] private TextOutlet makerText;
|
|
[SerializeField] private TextOutlet priceText;
|
|
[SerializeField] private TextOutlet holesText;
|
|
[SerializeField] private TextOutlet suiteText;
|
|
|
|
[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("Cash UI (assign any text fields to mirror cash amount)")]
|
|
[SerializeField] private List<UnityEngine.UI.Text> cashTextsLegacy = new List<UnityEngine.UI.Text>();
|
|
[SerializeField] private List<TMPro.TextMeshProUGUI> cashTextsTMP = new List<TMPro.TextMeshProUGUI>();
|
|
|
|
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;
|
|
private static bool s_hasPendingCash;
|
|
private static int s_pendingCashAmount;
|
|
|
|
private InventoryModel model;
|
|
private InventoryView view;
|
|
|
|
// === 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)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
return string.Empty;
|
|
|
|
StringBuilder result = new StringBuilder(text);
|
|
|
|
// Handle line breaks (\r)
|
|
result.Replace("\\r", "\n");
|
|
|
|
// Handle color codes (^RRGGBB format)
|
|
string processedText = ProcessColorCodes(result);
|
|
|
|
return processedText;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Format text for legacy Text components (limited rich text support)
|
|
/// </summary>
|
|
/// <param name="text">Raw text with formatting codes</param>
|
|
/// <returns>Formatted text for legacy Text</returns>
|
|
private static string FormatForLegacyText(string text)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
return string.Empty;
|
|
|
|
StringBuilder result = new StringBuilder(text);
|
|
|
|
// Handle line breaks (\r)
|
|
result.Replace("\\r", "\n");
|
|
|
|
// Handle color codes (^RRGGBB format) - convert to Unity's rich text format
|
|
string processedText = ProcessColorCodesForLegacy(result);
|
|
|
|
return processedText;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process color codes for TextMeshPro (supports hex colors directly)
|
|
/// </summary>
|
|
private static string ProcessColorCodes(StringBuilder text)
|
|
{
|
|
// Pattern to match color codes: ^ followed by 6 hex characters
|
|
string pattern = @"\^([0-9A-Fa-f]{6})";
|
|
|
|
return Regex.Replace(text.ToString(), pattern, match =>
|
|
{
|
|
string hexColor = match.Groups[1].Value;
|
|
return $"<color=#{hexColor}>";
|
|
}, RegexOptions.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process color codes for legacy Text (convert to Unity rich text format)
|
|
/// </summary>
|
|
private static string ProcessColorCodesForLegacy(StringBuilder text)
|
|
{
|
|
// Pattern to match color codes: ^ followed by 6 hex characters
|
|
string pattern = @"\^([0-9A-Fa-f]{6})";
|
|
|
|
return Regex.Replace(text.ToString(), pattern, match =>
|
|
{
|
|
string hexColor = match.Groups[1].Value;
|
|
// Convert hex to Unity color format
|
|
Color color = HexToColor(hexColor);
|
|
return $"<color=#{ColorUtility.ToHtmlStringRGB(color)}>";
|
|
}, RegexOptions.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert hex color string to Unity Color
|
|
/// </summary>
|
|
/// <param name="hex">Hex color string (e.g., "ffcb4a")</param>
|
|
/// <returns>Unity Color object</returns>
|
|
private static Color HexToColor(string hex)
|
|
{
|
|
if (hex.Length != 6)
|
|
return Color.white;
|
|
|
|
try
|
|
{
|
|
int r = Convert.ToInt32(hex.Substring(0, 2), 16);
|
|
int g = Convert.ToInt32(hex.Substring(2, 2), 16);
|
|
int b = Convert.ToInt32(hex.Substring(4, 2), 16);
|
|
|
|
return new Color(r / 255f, g / 255f, b / 255f, 1f);
|
|
}
|
|
catch
|
|
{
|
|
return Color.white;
|
|
}
|
|
}
|
|
|
|
// Current selected item for equip/unequip operations
|
|
private byte currentSelectedPackage;
|
|
private int currentSelectedSlot;
|
|
private EC_IvtrItem currentSelectedItem;
|
|
private EC_IvtrEquip 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 void Awake()
|
|
{
|
|
model = new InventoryModel();
|
|
view = new InventoryView();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
RefreshAll();
|
|
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();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (autoRefresh && Time.time - lastRefreshTime >= refreshInterval)
|
|
{
|
|
RefreshAll();
|
|
}
|
|
}
|
|
|
|
public void RefreshAll()
|
|
{
|
|
lastRefreshTime = Time.time;
|
|
|
|
var invItems = model.GetInventoryData(PKG_INVENTORY);
|
|
var eqpItems = model.GetInventoryData(PKG_EQUIPMENT);
|
|
var fshItems = model.GetInventoryData(PKG_FASHION);
|
|
|
|
view.RenderPackage(inventoryPackButtons, invItems, PKG_INVENTORY, OnInventoryButtonClicked, GetDisplayTextForItem);
|
|
view.RenderPackage(equipmentPackButtons, eqpItems, PKG_EQUIPMENT, OnInventoryButtonClicked, GetDisplayTextForItem);
|
|
view.RenderPackage(fashionPackButtons, fshItems, PKG_FASHION, OnInventoryButtonClicked, GetDisplayTextForItem);
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update all configured cash text components with the current boutique cash amount.
|
|
/// Call this when PLAYER_CASH arrives.
|
|
/// </summary>
|
|
public void UpdateCash(int amount)
|
|
{
|
|
string text = amount.ToString();
|
|
if (cashTextsLegacy != null)
|
|
{
|
|
for (int i = 0; i < cashTextsLegacy.Count; i++)
|
|
{
|
|
var t = cashTextsLegacy[i];
|
|
if (t != null) t.text = text;
|
|
}
|
|
}
|
|
if (cashTextsTMP != null)
|
|
{
|
|
for (int i = 0; i < cashTextsTMP.Count; i++)
|
|
{
|
|
var t = cashTextsTMP[i];
|
|
if (t != null) t.text = text;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void CacheCash(int amount)
|
|
{
|
|
s_pendingCashAmount = amount;
|
|
s_hasPendingCash = 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);
|
|
}
|
|
if (s_hasPendingCash)
|
|
{
|
|
UpdateCash(s_pendingCashAmount);
|
|
}
|
|
}
|
|
|
|
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
|
|
PositionDetailPanelNearButton(package, slot);
|
|
|
|
FillDetailPanel(package, itemData);
|
|
}
|
|
else
|
|
{
|
|
ShowDetailPanel(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create EC_IvtrEquip object from InventoryItemData
|
|
/// </summary>
|
|
private EC_IvtrEquip CreateEquipmentFromItemData(EC_IvtrItem itemData)
|
|
{
|
|
if (itemData == null)
|
|
return null;
|
|
|
|
var equipment = new EC_IvtrEquip(itemData.m_tid, itemData.m_expire_date);
|
|
|
|
// Set basic properties (use default values since InventoryItemData doesn't have these)
|
|
equipment.Price = 0;
|
|
equipment.Count = itemData.m_iCount;
|
|
equipment.PriceScale = 1.0f;
|
|
equipment.ScaleType = 0;
|
|
|
|
// 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 equip/unequip operation");
|
|
return;
|
|
}
|
|
|
|
if (currentSelectedPackage == PKG_INVENTORY)
|
|
{
|
|
// Equipping from inventory
|
|
EquipItem();
|
|
}
|
|
else if (currentSelectedPackage == PKG_EQUIPMENT)
|
|
{
|
|
// Unequipping from equipment
|
|
UnequipItem();
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[InventoryUI] Equip/Unequip not supported for package {currentSelectedPackage}");
|
|
}
|
|
}
|
|
|
|
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}");
|
|
}
|
|
}
|
|
|
|
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
|
|
{
|
|
private readonly FieldInfo itemsByPackageField;
|
|
|
|
public InventoryModel()
|
|
{
|
|
var inventoryType = typeof(EC_Inventory);
|
|
itemsByPackageField = inventoryType.GetField("_itemsByPackage", BindingFlags.NonPublic | BindingFlags.Static);
|
|
if (itemsByPackageField == null)
|
|
{
|
|
Debug.LogError("[InventoryUI] Could not access _itemsByPackage field from EC_Inventory");
|
|
}
|
|
}
|
|
|
|
public Dictionary<int, EC_IvtrItem> GetInventoryData(byte package)
|
|
{
|
|
if (itemsByPackageField == null)
|
|
{
|
|
return new Dictionary<int, EC_IvtrItem>();
|
|
}
|
|
var itemsByPackage = itemsByPackageField.GetValue(null) as Dictionary<byte, Dictionary<int, EC_IvtrItem>>;
|
|
if (itemsByPackage == null)
|
|
{
|
|
return new Dictionary<int, EC_IvtrItem>();
|
|
}
|
|
if (itemsByPackage.TryGetValue(package, out var packageItems))
|
|
{
|
|
return packageItems;
|
|
}
|
|
return new Dictionary<int, EC_IvtrItem>();
|
|
}
|
|
}
|
|
|
|
// === MVC: View ===
|
|
private class InventoryView
|
|
{
|
|
private static readonly Dictionary<Image, Sprite> _defaultSprites = new Dictionary<Image, Sprite>();
|
|
|
|
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));
|
|
// Optional visual tweaks based on state/count
|
|
var image = button.GetComponent<Image>();
|
|
if (image != null)
|
|
{
|
|
// Store the default sprite if we haven't seen this image before
|
|
if (!_defaultSprites.ContainsKey(image))
|
|
{
|
|
_defaultSprites[image] = image.sprite;
|
|
}
|
|
|
|
// Set icon sprite based on item TemplateId
|
|
if (hasItem && itemData != null && itemData.m_iCount > 0)
|
|
{
|
|
var sprite = EC_IvtrItemUtils.Instance.ResolveItemIconSprite(itemData.m_tid);
|
|
image.sprite = sprite;
|
|
image.enabled = true;
|
|
}
|
|
else
|
|
{
|
|
// Restore the default sprite instead of setting to null
|
|
image.sprite = _defaultSprites[image];
|
|
image.enabled = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// === Detail Panel Helpers ===
|
|
[System.Serializable]
|
|
private class TextOutlet
|
|
{
|
|
[SerializeField] public Text legacy;
|
|
[SerializeField] public TMPro.TextMeshProUGUI tmp;
|
|
|
|
public void Set(string value)
|
|
{
|
|
if (legacy != null)
|
|
{
|
|
legacy.text = FormatForLegacyText(value ?? string.Empty);
|
|
}
|
|
if (tmp != null)
|
|
{
|
|
tmp.text = FormatForTextMeshPro(value ?? string.Empty);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set text with explicit formatting preference
|
|
/// </summary>
|
|
/// <param name="value">Raw text with formatting codes</param>
|
|
/// <param name="preferTextMeshPro">Whether to prefer TextMeshPro formatting</param>
|
|
public void SetFormatted(string value, bool preferTextMeshPro = true)
|
|
{
|
|
string formattedText = preferTextMeshPro ?
|
|
FormatForTextMeshPro(value ?? string.Empty) :
|
|
FormatForLegacyText(value ?? string.Empty);
|
|
|
|
if (legacy != null)
|
|
{
|
|
legacy.text = formattedText;
|
|
}
|
|
if (tmp != null)
|
|
{
|
|
tmp.text = formattedText;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ShowDetailPanel(bool show)
|
|
{
|
|
if (detailPanelRoot != null)
|
|
{
|
|
detailPanelRoot.SetActive(show);
|
|
}
|
|
}
|
|
|
|
private Button GetButtonForSlot(byte package, int slot)
|
|
{
|
|
List<Button> list = null;
|
|
switch (package)
|
|
{
|
|
case PKG_INVENTORY:
|
|
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;
|
|
}
|
|
|
|
var canvas = panelRect.GetComponentInParent<Canvas>();
|
|
if (canvas == null)
|
|
{
|
|
return;
|
|
}
|
|
var parentRect = panelRect.parent as RectTransform;
|
|
if (parentRect == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Camera eventCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
|
|
Vector3 worldCenter = buttonRect.TransformPoint(buttonRect.rect.center);
|
|
Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(eventCamera, worldCenter);
|
|
Vector2 localPoint;
|
|
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRect, screenPoint, eventCamera, out localPoint))
|
|
{
|
|
return;
|
|
}
|
|
|
|
float btnHalfW = buttonRect.rect.width * 0.5f;
|
|
float panelW = panelRect.rect.width;
|
|
float panelH = panelRect.rect.height;
|
|
float pivotX = panelRect.pivot.x;
|
|
float pivotY = panelRect.pivot.y;
|
|
|
|
// Compute right-placement candidate (panel's left edge at button's right edge + offset)
|
|
float leftEdgeRightPlacement = localPoint.x + btnHalfW + detailPanelOffset.x;
|
|
float candidateXRight = leftEdgeRightPlacement + pivotX * panelW;
|
|
|
|
// Compute left-placement candidate (panel's right edge at button's left edge - offset)
|
|
float rightEdgeLeftPlacement = localPoint.x - btnHalfW - detailPanelOffset.x;
|
|
float candidateXLeft = rightEdgeLeftPlacement - (1f - pivotX) * panelW;
|
|
|
|
// Vertical clamping honoring pivot
|
|
float minY = parentRect.rect.yMin + pivotY * panelH;
|
|
float maxY = parentRect.rect.yMax - (1f - pivotY) * panelH;
|
|
float candidateY = Mathf.Clamp(localPoint.y + detailPanelOffset.y, minY, maxY);
|
|
|
|
// Choose side based on available space
|
|
float rightEdgeOfRight = candidateXRight + (1f - pivotX) * panelW;
|
|
float canvasRight = parentRect.rect.xMax;
|
|
float canvasLeft = parentRect.rect.xMin;
|
|
float leftEdgeOfLeft = candidateXLeft - pivotX * panelW;
|
|
|
|
Vector2 finalPos;
|
|
if (rightEdgeOfRight <= canvasRight)
|
|
{
|
|
finalPos = new Vector2(candidateXRight, candidateY);
|
|
}
|
|
else if (leftEdgeOfLeft >= canvasLeft)
|
|
{
|
|
finalPos = new Vector2(candidateXLeft, candidateY);
|
|
}
|
|
else
|
|
{
|
|
// Fallback: clamp within canvas horizontally
|
|
float minX = canvasLeft + pivotX * panelW;
|
|
float maxX = canvasRight - (1f - pivotX) * panelW;
|
|
finalPos = new Vector2(Mathf.Clamp(candidateXRight, minX, maxX), candidateY);
|
|
}
|
|
|
|
panelRect.anchoredPosition = finalPos;
|
|
}
|
|
|
|
private void FillDetailPanel(byte package, EC_IvtrItem item)
|
|
{
|
|
if (item == null)
|
|
{
|
|
ShowDetailPanel(false);
|
|
return;
|
|
}
|
|
|
|
// Get user-friendly text from string tables
|
|
string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(item.m_tid);
|
|
string itemDescription = GetItemDescription(item.m_tid);
|
|
string itemExtendedDesc = GetItemExtendedDescription(item.m_tid);
|
|
|
|
// Display basic content
|
|
nameText?.Set(string.IsNullOrEmpty(itemName) ? $"Item {item.m_tid}" : itemName);
|
|
descriptionText?.Set(itemDescription ?? "No description available");
|
|
extendedDescText?.Set(itemExtendedDesc ?? "");
|
|
countText?.Set($"Count: {item.m_iCount}");
|
|
|
|
// Format state properly
|
|
string stateTextValue = GetItemStateText(item.State);
|
|
stateText?.Set(stateTextValue);
|
|
|
|
// Format expiration date properly
|
|
if (item.m_expire_date > 0)
|
|
{
|
|
DateTime expireDate = DateTimeOffset.FromUnixTimeSeconds(item.m_expire_date).DateTime;
|
|
expireText?.Set($"Expires: {expireDate:yyyy-MM-dd HH:mm}");
|
|
}
|
|
else
|
|
{
|
|
expireText?.Set("No expiration");
|
|
}
|
|
|
|
// Display equipment-specific information
|
|
if (showEquipmentDetails && currentSelectedEquipment != null)
|
|
{
|
|
FillEquipmentDetails();
|
|
}
|
|
else
|
|
{
|
|
ClearEquipmentDetails();
|
|
}
|
|
|
|
// Setup equip and drop buttons
|
|
SetupEquipButton(package, item);
|
|
SetupDropButton(package, item);
|
|
|
|
ShowDetailPanel(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fill equipment-specific details
|
|
/// </summary>
|
|
private void FillEquipmentDetails()
|
|
{
|
|
if (currentSelectedEquipment == null)
|
|
{
|
|
ClearEquipmentDetails();
|
|
return;
|
|
}
|
|
|
|
// Level Requirements
|
|
if (levelReqText != null)
|
|
{
|
|
string levelReq = "";
|
|
if (currentSelectedEquipment.LevelReq > 0)
|
|
{
|
|
levelReq = $"Level: {currentSelectedEquipment.LevelReq}";
|
|
}
|
|
if (currentSelectedEquipment.ProfReq > 0)
|
|
{
|
|
if (!string.IsNullOrEmpty(levelReq)) levelReq += "\\r";
|
|
levelReq += $"Profession: {currentSelectedEquipment.ProfReq}";
|
|
}
|
|
levelReqText.Set(levelReq);
|
|
}
|
|
|
|
// Stats Requirements
|
|
if (statsReqText != null)
|
|
{
|
|
List<string> reqs = new List<string>();
|
|
if (currentSelectedEquipment.StrengthReq > 0) reqs.Add($"Str: {currentSelectedEquipment.StrengthReq}");
|
|
if (currentSelectedEquipment.AgilityReq > 0) reqs.Add($"Agi: {currentSelectedEquipment.AgilityReq}");
|
|
if (currentSelectedEquipment.VitalityReq > 0) reqs.Add($"Vit: {currentSelectedEquipment.VitalityReq}");
|
|
if (currentSelectedEquipment.EnergyReq > 0) reqs.Add($"Ene: {currentSelectedEquipment.EnergyReq}");
|
|
if (currentSelectedEquipment.ReputationReq > 0) reqs.Add($"Rep: {currentSelectedEquipment.ReputationReq}");
|
|
|
|
statsReqText.Set(string.Join("\\r", reqs));
|
|
}
|
|
|
|
// Endurance
|
|
if (enduranceText != null)
|
|
{
|
|
if (currentSelectedEquipment.MaxEndurance > 0)
|
|
{
|
|
int curEndurance = EC_IvtrEquip.VisualizeEndurance(currentSelectedEquipment.CurEndurance);
|
|
int maxEndurance = EC_IvtrEquip.VisualizeEndurance(currentSelectedEquipment.MaxEndurance);
|
|
string endurance = $"Endurance: {curEndurance}/{maxEndurance}";
|
|
|
|
if (currentSelectedEquipment.IsDestroying())
|
|
{
|
|
endurance += " (DESTROYED)";
|
|
}
|
|
else if (currentSelectedEquipment.CurEndurance < currentSelectedEquipment.MaxEndurance)
|
|
{
|
|
endurance += " (Damaged)";
|
|
}
|
|
|
|
enduranceText.Set(endurance);
|
|
}
|
|
else
|
|
{
|
|
enduranceText.Set("Endurance: N/A");
|
|
}
|
|
}
|
|
|
|
// Repair Cost
|
|
if (repairCostText != null)
|
|
{
|
|
if (currentSelectedEquipment.IsRepairable())
|
|
{
|
|
int repairCost = currentSelectedEquipment.GetRepairCost();
|
|
repairCostText.Set($"Repair Cost: {repairCost}");
|
|
}
|
|
else
|
|
{
|
|
repairCostText.Set("Repair Cost: N/A");
|
|
}
|
|
}
|
|
|
|
// Properties + Base Stats + Engraved + Stones (more closely matching original)
|
|
if (propertiesText != null)
|
|
{
|
|
List<string> lines = new List<string>();
|
|
|
|
// Base stats decoded from element essence (damage/defense/speed/range/resists)
|
|
string baseStats = currentSelectedEquipment.GetBaseStatsForDisplay();
|
|
if (!string.IsNullOrEmpty(baseStats))
|
|
{
|
|
lines.Add(baseStats);
|
|
}
|
|
|
|
// Add-on properties from detail payload (non-embedded, non-suite, non-engraved)
|
|
if (currentSelectedEquipment.Props.Count > 0)
|
|
{
|
|
foreach (var prop in currentSelectedEquipment.Props)
|
|
{
|
|
if (!prop.Embed && !prop.Suite && !prop.Engraved)
|
|
{
|
|
string propDesc = currentSelectedEquipment.FormatPropDesc(prop);
|
|
if (!string.IsNullOrEmpty(propDesc))
|
|
{
|
|
lines.Add(propDesc);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Engraved properties (displayed after normal add-ons)
|
|
foreach (var prop in currentSelectedEquipment.Props)
|
|
{
|
|
if (prop.Engraved)
|
|
{
|
|
string propDesc = currentSelectedEquipment.FormatPropDesc(prop);
|
|
if (!string.IsNullOrEmpty(propDesc))
|
|
{
|
|
lines.Add(propDesc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Socketed stones (show name and a basic description)
|
|
if (currentSelectedEquipment.Holes != null && currentSelectedEquipment.Holes.Count > 0)
|
|
{
|
|
foreach (int holeTid in currentSelectedEquipment.Holes)
|
|
{
|
|
if (holeTid == 0) continue;
|
|
string stoneName = EC_IvtrItemUtils.Instance.ResolveItemName(holeTid);
|
|
// Try to fetch a description from string tables; fallback to name if unavailable
|
|
string stoneDesc = GetItemDescription(holeTid) ?? stoneName;
|
|
if (!string.IsNullOrEmpty(stoneName))
|
|
{
|
|
lines.Add($"{stoneName}: {stoneDesc}");
|
|
}
|
|
}
|
|
}
|
|
|
|
string combined = string.Join("\\r", lines);
|
|
propertiesText.Set(combined);
|
|
}
|
|
|
|
// Refinement
|
|
if (refineText != null)
|
|
{
|
|
if (currentSelectedEquipment.RefineLvl > 0)
|
|
{
|
|
refineText.Set($"Refinement Level: +{currentSelectedEquipment.RefineLvl}");
|
|
}
|
|
else
|
|
{
|
|
refineText.Set("Refinement: None");
|
|
}
|
|
}
|
|
|
|
// Maker Information
|
|
if (makerText != null)
|
|
{
|
|
if (!string.IsNullOrEmpty(currentSelectedEquipment.Maker))
|
|
{
|
|
makerText.Set($"Maker: {currentSelectedEquipment.Maker}");
|
|
}
|
|
else
|
|
{
|
|
makerText.Set("Maker: Unknown");
|
|
}
|
|
}
|
|
|
|
// Price
|
|
if (priceText != null)
|
|
{
|
|
int scaledPrice = currentSelectedEquipment.GetScaledPrice();
|
|
priceText.Set($"Price: {scaledPrice}");
|
|
}
|
|
|
|
// Holes
|
|
if (holesText != null)
|
|
{
|
|
if (currentSelectedEquipment.Holes.Count > 0)
|
|
{
|
|
int emptyHoles = currentSelectedEquipment.GetEmptyHoleNum();
|
|
int totalHoles = currentSelectedEquipment.Holes.Count;
|
|
holesText.Set($"Holes: {totalHoles - emptyHoles}/{totalHoles} filled");
|
|
}
|
|
else
|
|
{
|
|
holesText.Set("Holes: None");
|
|
}
|
|
}
|
|
|
|
// Suite Information
|
|
if (suiteText != null)
|
|
{
|
|
int suiteId = currentSelectedEquipment.GetSuiteID();
|
|
if (suiteId > 0)
|
|
{
|
|
suiteText.Set($"Suite Equipment: {suiteId}");
|
|
}
|
|
else
|
|
{
|
|
suiteText.Set("Suite: None");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear equipment details
|
|
/// </summary>
|
|
private void ClearEquipmentDetails()
|
|
{
|
|
levelReqText?.Set("");
|
|
statsReqText?.Set("");
|
|
enduranceText?.Set("");
|
|
repairCostText?.Set("");
|
|
propertiesText?.Set("");
|
|
refineText?.Set("");
|
|
makerText?.Set("");
|
|
priceText?.Set("");
|
|
holesText?.Set("");
|
|
suiteText?.Set("");
|
|
}
|
|
|
|
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)
|
|
{
|
|
tmpText.text = "Equip";
|
|
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)
|
|
{
|
|
buttonText.text = "Equip";
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|