Files
test/Assets/PerfectWorld/Scripts/UI/ShopUIManager.cs
2026-04-13 11:00:15 +07:00

624 lines
20 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using BrewMonster.Network;
public class ShopUIManager : MonoBehaviour
{
[Header("Shop Data")]
public GShopLoader shopLoader;
[Header("UI Panels")]
public GameObject shopMainPanel;
public GameObject shopDetailPanel;
public GameObject subTypeShopPanel;
[Header("Item Display")]
public Transform itemContainer; // Parent for item panels
public GameObject itemPanelPrefab; // Prefab for individual item panels
[Header("Object Pooling")]
public ItemPanelPool itemPanelPool; // Object pool for item panels
public bool useObjectPooling = true;
[Header("Category Buttons")]
public Button[] categoryButtons; // 6 buttons for categories
[Header("Detail Panel Components")]
public TextMeshProUGUI detailItemName;
public TextMeshProUGUI detailItemDescription;
public Image detailItemIcon;
public TextMeshProUGUI detailItemPrice;
public TextMeshProUGUI detailItemQuantity;
public Button buyButton;
public Button closeDetailButton;
public ScrollRect detailScrollRect;
[Header("Main Panel Components")]
public Button closeShopButton;
[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>();
// Keep last known cash so newly opened shops can render immediately.
private static bool s_hasPendingCash;
private static int s_pendingCashAmount;
private List<GameObject> currentItemPanels = new List<GameObject>();
private GShopItem selectedItem;
private int currentCategory = 0; // 0-5 for the 6 merged categories
[Header("Sub Type Shop")]
//-1 means all sub types
public SubTypeShop subTypeShop;
private int currentSubType = -1;
private bool _shopLoaderEventsSubscribed;
void Start()
{
InitializeUI();
SetupEventListeners();
InitializePool();
SubscribeShopLoaderEvents();
}
void SubscribeShopLoaderEvents()
{
if (_shopLoaderEventsSubscribed)
return;
if (shopLoader == null)
shopLoader = FindFirstObjectByType<GShopLoader>();
if (shopLoader == null)
return;
shopLoader.OnPrimaryShopLoaded += OnPrimaryShopDataReady;
_shopLoaderEventsSubscribed = true;
// Load may have finished before we subscribed (e.g. script order).
if (shopLoader.IsPrimaryShopLoaded)
OnPrimaryShopDataReady();
}
void OnPrimaryShopDataReady()
{
if (shopMainPanel != null && shopMainPanel.activeSelf)
RefreshShopDisplay();
}
void InitializePool()
{
if (useObjectPooling && itemPanelPool == null)
{
// Auto-create pool if not assigned
GameObject poolObj = new GameObject("ItemPanelPool");
poolObj.transform.SetParent(transform);
itemPanelPool = poolObj.AddComponent<ItemPanelPool>();
itemPanelPool.itemPanelPrefab = itemPanelPrefab;
}
}
void InitializeUI()
{
// Initially hide panels
if (shopMainPanel != null)
shopMainPanel.SetActive(false);
if (shopDetailPanel != null)
shopDetailPanel.SetActive(false);
if (subTypeShop != null)
subTypeShopPanel.SetActive(false);
}
void SetupEventListeners()
{
// Setup category buttons
for (int i = 0; i < categoryButtons.Length && i < 6; i++)
{
int categoryIndex = i; // Capture for closure
categoryButtons[i].onClick.AddListener(() => OnCategorySelected(categoryIndex));
}
// Setup detail panel buttons
if (buyButton != null)
buyButton.onClick.AddListener(OnBuyItem);
if (closeDetailButton != null)
closeDetailButton.onClick.AddListener(CloseDetailPanel);
// Setup main panel close button
if (closeShopButton != null)
closeShopButton.onClick.AddListener(CloseShop);
}
public void OpenShop()
{
if (shopMainPanel != null)
{
SubscribeShopLoaderEvents();
shopMainPanel.SetActive(true);
// Always apply category 0 when opening; OnCategorySelected(0) no-ops if currentCategory is already 0.
OnCategorySelected(0, forceRefresh: true);
ApplyPendingCash();
UnityGameSession.RequesrQueryPlayerCash();
}
}
public void UpdateCash(int amount)
{
string text = amount.ToString();
int major = amount / 100; // e.g. 49780 -> 497 (KNB)
int minor = amount % 100; // e.g. 49780 -> 80 (NNB)
string textMajor = major.ToString();
string textMinor = minor.ToString();
// For now only show the first part (major = 497)
if (cashTextsLegacy != null)
{
for (int i = 0; i < cashTextsLegacy.Count; i++)
{
var t = cashTextsLegacy[i];
if (t != null) t.text = text;
if (t != null) t.text = textMajor;
}
}
if (cashTextsTMP != null)
for (int i = 0; i < cashTextsTMP.Count; i++)
{
var t = cashTextsTMP[i];
if (t != null) t.text = text;
if (t != null) t.text = textMajor;
}
}
public static void CacheCash(int amount)
{
s_pendingCashAmount = amount;
s_hasPendingCash = true;
var all = Resources.FindObjectsOfTypeAll<ShopUIManager>();
if (all != null)
{
for (int i = 0; i < all.Length; i++)
{
var ui = all[i];
if (ui != null && ui.gameObject.scene.IsValid())
{
ui.ApplyPendingCash();
}
}
}
}
private void ApplyPendingCash()
{
if (s_hasPendingCash)
{
UpdateCash(s_pendingCashAmount);
}
}
public void CloseShop()
{
if (shopMainPanel != null)
shopMainPanel.SetActive(false);
if (shopDetailPanel != null)
shopDetailPanel.SetActive(false);
}
void OnCategorySelected(int categoryIndex, bool forceRefresh = false)
{
if (!forceRefresh && categoryIndex == currentCategory)
return;
float startTime = Time.realtimeSinceStartup;
currentCategory = categoryIndex;
RefreshShopDisplay();
// Update button states
for (int i = 0; i < categoryButtons.Length; i++)
{
if (categoryButtons[i] != null)
{
categoryButtons[i].interactable = (i != categoryIndex);
}
}
// Log performance
float switchTime = Time.realtimeSinceStartup - startTime;
Debug.Log($"Category switch to {categoryIndex} completed in {switchTime * 1000f:F2}ms");
}
// Allow external components (e.g., ShopCategoryManager) to switch category
public void SetCategoryIndex(int categoryIndex)
{
OnCategorySelected(categoryIndex);
}
public void RefreshShopDisplay()
{
// Return all current panels to pool
ReturnAllPanelsToPool();
if (shopLoader == null || shopLoader.primaryShop == null)
{
Debug.LogWarning("ShopLoader or primary shop data not available");
return;
}
// Get items for current category
List<GShopItem> categoryItems = GetItemsForCategory(currentCategory);
// Create item panels using pooling
int siblingIndexCounter = 0;
foreach (GShopItem item in categoryItems)
{
CreateItemPanelFromPool(item, siblingIndexCounter);
siblingIndexCounter++;
}
List<string> subTypeNames = GetSubTypeNamesForCategory(currentCategory);
currentSubType = -1;
subTypeShopPanel.SetActive(subTypeNames.Count > 0);
if (subTypeShop != null)
subTypeShop.GenerateSubTypeShop(subTypeNames, this);
}
public void SetSubTypeFilter(int subTypeIndex)
{
currentSubType = subTypeIndex;
ApplySubTypeFilter();
}
public void SetSubTypeFilterByName(string subTypeName)
{
int subTypeIndex = GetSubTypeIndexFromName(subTypeName);
SetSubTypeFilter(subTypeIndex);
}
public void ApplySubTypeFilter()
{
ReturnAllPanelsToPool();
if (shopLoader == null || shopLoader.primaryShop == null)
{
Debug.LogWarning("ShopLoader or primary shop data not available");
return;
}
if(currentSubType == -1)
{
RefreshShopDisplay();
return;
}
List<GShopItem> categoryItems = GetItemsForCategory(currentCategory, currentSubType);
int siblingIndexCounter = 0;
foreach (GShopItem item in categoryItems)
{
CreateItemPanelFromPool(item, siblingIndexCounter);
siblingIndexCounter++;
}
}
List<GShopItem> GetItemsForCategory(int categoryIndex, int subTypeIndex=-1)
{
List<GShopItem> items = new List<GShopItem>();
if (shopLoader.primaryShop.items == null)
return items;
foreach (GShopItem item in shopLoader.primaryShop.items)
{
if (IsItemInCategory(item, categoryIndex))
{
if(IsItemInSubType(item, subTypeIndex))
{
items.Add(item);
}
}
}
return items;
}
List<string> GetSubTypeNamesForCategory(int categoryIndex)
{
List<string> subTypeNames = new List<string>();
if (shopLoader?.primaryShop?.mainTypes == null)
return subTypeNames;
// Use the same mapping as IsItemInCategory: 0=0, 1=2, 2=5, 3=1+3+4, 4=6, 5=7
List<int> mainTypeIndices = new List<int>();
switch (categoryIndex)
{
case 0: mainTypeIndices.Add(0); break;
case 1: mainTypeIndices.Add(2); break;
case 2: mainTypeIndices.Add(5); break;
case 3: mainTypeIndices.Add(1); mainTypeIndices.Add(3); mainTypeIndices.Add(4); break;
case 4: mainTypeIndices.Add(6); break;
case 5: mainTypeIndices.Add(7); break;
default: break;
}
foreach (int mainTypeIndex in mainTypeIndices)
{
if (mainTypeIndex >= 0 && mainTypeIndex < shopLoader.primaryShop.mainTypes.Count)
{
foreach (string subType in shopLoader.primaryShop.mainTypes[mainTypeIndex].subTypes)
{
if (!subTypeNames.Contains(subType))
{
subTypeNames.Add(subType);
}
}
}
}
return subTypeNames;
}
bool IsItemInCategory(GShopItem item, int categoryIndex)
{
// Category mapping: 0=1, 1=2, 2=1+3+4, 3=6, 4=7, 5=8
switch (categoryIndex)
{
case 0: return item.mainType == 0; // Category 1
case 1: return item.mainType == 2; // Category 2
case 2: return item.mainType == 5; // Categories 1, 3, 4 merged
case 3: return item.mainType == 1 || item.mainType == 3 || item.mainType == 4; // Category 6
case 4: return item.mainType == 6; // Category 7
case 5: return item.mainType == 7; // Category 8
default: return false;
}
}
bool IsItemInSubType(GShopItem item, int combinedSubTypeIndex)
{
if(combinedSubTypeIndex == -1)
return true;
// Get the subType name for this item's mainType and subType index
if (shopLoader?.primaryShop?.mainTypes == null)
return false;
if (item.mainType < 0 || item.mainType >= shopLoader.primaryShop.mainTypes.Count)
return false;
var mainType = shopLoader.primaryShop.mainTypes[item.mainType];
if (item.subType < 0 || item.subType >= mainType.subTypes.Count)
return false;
string itemSubTypeName = mainType.subTypes[item.subType];
// Get the subType name for the filter index
string filterSubTypeName = GetSubTypeNameFromIndex(combinedSubTypeIndex);
return itemSubTypeName == filterSubTypeName;
}
int GetSubTypeIndexFromName(string subTypeName)
{
if (shopLoader?.primaryShop?.mainTypes == null || string.IsNullOrEmpty(subTypeName))
return -1;
// Find the subType name in the current category's mainTypes
List<int> mainTypeIndices = new List<int>();
switch (currentCategory)
{
case 0: mainTypeIndices.Add(0); break;
case 1: mainTypeIndices.Add(2); break;
case 2: mainTypeIndices.Add(5); break;
case 3: mainTypeIndices.Add(1); mainTypeIndices.Add(3); mainTypeIndices.Add(4); break;
case 4: mainTypeIndices.Add(6); break;
case 5: mainTypeIndices.Add(7); break;
default: break;
}
// Find the first matching subType across all relevant mainTypes
// We use a combined index: mainTypeIndex * 1000 + subTypeIndex
// This allows us to uniquely identify subTypes across different mainTypes
foreach (int mainTypeIndex in mainTypeIndices)
{
if (mainTypeIndex >= 0 && mainTypeIndex < shopLoader.primaryShop.mainTypes.Count)
{
var mainType = shopLoader.primaryShop.mainTypes[mainTypeIndex];
for (int i = 0; i < mainType.subTypes.Count; i++)
{
if (mainType.subTypes[i] == subTypeName)
{
// Return a combined index: mainTypeIndex * 1000 + subTypeIndex
return mainTypeIndex * 1000 + i;
}
}
}
}
return -1;
}
string GetSubTypeNameFromIndex(int combinedIndex)
{
if (combinedIndex < 0 || shopLoader?.primaryShop?.mainTypes == null)
return null;
int mainTypeIndex = combinedIndex / 1000;
int subTypeIndex = combinedIndex % 1000;
if (mainTypeIndex >= 0 && mainTypeIndex < shopLoader.primaryShop.mainTypes.Count)
{
var mainType = shopLoader.primaryShop.mainTypes[mainTypeIndex];
if (subTypeIndex >= 0 && subTypeIndex < mainType.subTypes.Count)
{
return mainType.subTypes[subTypeIndex];
}
}
return null;
}
void CreateItemPanelFromPool(GShopItem item, int targetSiblingIndex)
{
GameObject itemPanel = null;
if (useObjectPooling && itemPanelPool != null)
{
// Get panel from pool
itemPanel = itemPanelPool.GetPanel();
}
else
{
// Fallback to instantiate if pooling disabled
if (itemPanelPrefab != null && itemContainer != null)
{
itemPanel = Instantiate(itemPanelPrefab, itemContainer);
}
}
if (itemPanel != null && itemContainer != null)
{
// Set parent and position
itemPanel.transform.SetParent(itemContainer, false);
itemPanel.transform.localScale = Vector3.one;
// Ensure deterministic ordering regardless of pool retrieval order
itemPanel.transform.SetSiblingIndex(targetSiblingIndex);
// Setup the panel
ShopItemPanel itemPanelScript = itemPanel.GetComponent<ShopItemPanel>();
if (itemPanelScript != null)
{
itemPanelScript.SetupItem(item, this);
}
currentItemPanels.Add(itemPanel);
}
}
void ReturnAllPanelsToPool()
{
if (useObjectPooling && itemPanelPool != null)
{
// Return all panels to pool
foreach (GameObject panel in currentItemPanels)
{
if (panel != null)
{
itemPanelPool.ReturnPanel(panel);
}
}
}
else
{
// Destroy panels if not using pooling
foreach (GameObject panel in currentItemPanels)
{
if (panel != null)
Destroy(panel);
}
}
currentItemPanels.Clear();
}
public void ShowItemDetail(GShopItem item)
{
selectedItem = item;
Debug.Log($"$ Local ID of selected item: {item.localId}");
if (shopDetailPanel != null)
{
shopDetailPanel.SetActive(true);
// Get the ShopDetailPanel component and call its SetupDetailPanel method
ShopDetailPanel detailPanelScript = shopDetailPanel.GetComponent<ShopDetailPanel>();
if (detailPanelScript != null)
{
detailPanelScript.SetupDetailPanel(item, this);
}
else
{
Debug.LogError("ShopDetailPanel script not found on shopDetailPanel GameObject!");
// Fallback to the old method
UpdateDetailPanel(item);
detailScrollRect.verticalNormalizedPosition = 1f;
LayoutRebuilder.ForceRebuildLayoutImmediate(detailScrollRect.content);
}
}
}
void UpdateDetailPanel(GShopItem item)
{
if (detailItemName != null)
detailItemName.text = item.name;
if (detailItemDescription != null)
detailItemDescription.text = item.desc;
if (detailItemQuantity != null)
detailItemQuantity.text = $"Quantity: {item.num}";
// Find the best buy option (first non-zero price)
uint bestPrice = 0;
for (int i = 0; i < item.buy.Length; i++)
{
if (item.buy[i].price > 0)
{
bestPrice = item.buy[i].price;
break;
}
}
if (detailItemPrice != null)
detailItemPrice.text = $"Price: {bestPrice}";
Debug.Log($"Detail item price: {detailItemPrice.text}, detail item price != null: {detailItemPrice != null}");
// Load icon based on item name (you'll need to implement icon loading)
if (detailItemIcon != null)
{
LoadItemIcon(detailItemIcon, item.name);
}
}
void LoadItemIcon(Image iconImage, string itemName)
{
// TODO: Implement icon loading based on item name
// This is where you'd load the appropriate icon sprite
// For now, we'll leave it empty for you to implement
Debug.Log($"Loading icon for item: {itemName}");
}
void CloseDetailPanel()
{
if (shopDetailPanel != null)
shopDetailPanel.SetActive(false);
}
void OnBuyItem()
{
if (selectedItem.id == 0)
return;
// TODO: Implement purchase logic
Debug.Log($"Attempting to buy item: {selectedItem.name} (ID: {selectedItem.id})");
// Close detail panel after purchase attempt
CloseDetailPanel();
}
void OnDestroy()
{
if (shopLoader != null && _shopLoaderEventsSubscribed)
shopLoader.OnPrimaryShopLoaded -= OnPrimaryShopDataReady;
// Clean up pooled objects
if (useObjectPooling && itemPanelPool != null)
{
itemPanelPool.ReturnAllPanels();
}
else
{
// Fallback cleanup
foreach (GameObject panel in currentItemPanels)
{
if (panel != null)
Destroy(panel);
}
}
currentItemPanels.Clear();
}
}