using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using Bansonic; using UnityEngine; using UnityEngine.UI; public class storeSystem : MonoBehaviour { private static readonly Regex RichTextTagRegex = new Regex("<.*?>", RegexOptions.Compiled); private static readonly string[] SortDropdownOptionLabels = { "默认排序", "价格升序", "价格降序", "类型升序", "类型降序", "限购升序", "限购降序" }; private const string ShowOnlyPurchasablePrefKey = "store.show_only_can_purchase"; private const string SortDropdownPrefKey = "store.sort_dropdown_value"; private const string StoreStateSaveCategory = "store_state"; private const string StoreStateSaveKey = "runtime"; [Header("Player Data")] [SerializeField] private Player_SO playerData; [Header("closeButton")] public Button closeButton; [Header("filter")] public Toggle show_only_canPurchase; public Dropdown sortDropdown; [Header("list toggles")] public ToggleGroup toggleGroup; public Toggle allItems_toggle; public Toggle recentlyHot_toggle; public Toggle idols_toggle; public Toggle songs_toggle; public Toggle storyPassage_toggle; public Toggle consumable_toggle; public Toggle otherType_toggle; [Header("objects")] public GameObject itemPrefab; public Transform contentParent; [Header("paths")] [SerializeField] private string editorStoreItemPath; [SerializeField] private string runtimeStoreItemPath; [Header("sprites")] public Sprite coinSprite; public Sprite materialSprite; public Sprite cannotBuySprite; public Sprite memoryBuyPlanSprite; public Sprite memorySellPlanSprite; [Header("selecting item purchase")] public Text p_itemName; [SerializeField] private bool p_itemNameFollowRichTextColor = true; public Image p_itemImage; public Image p_itemRarityBtmImage; public Sprite[] raritybtmImageSprite; public Image p_sumCostImage; public Text p_sumCostText; public Button p_iAmount_plus; public Button p_iAmount_minus; public InputField p_iAmount_input; public Button purchaseButton; public Text p_itemUsageText; public Text p_detailedDescriptionText; [Header("selectable equipment reward")] public GameObject ctasPrefab; public Transform ctasParent; private const int MinPurchaseAmount = 1; private const int MaxPurchaseAmount = 999999; private static readonly Color InsufficientCostColor = new Color(0.45f, 0.08f, 0.08f, 1f); private readonly List cachedItems = new List(); private readonly List toggles = new List(); private StoreRuntimeSaveData persistedState = new StoreRuntimeSaveData(); private storeItemSO currentSelectedItem; private int currentSelectedItemId = -1; private int currentPurchaseAmount = MinPurchaseAmount; private bool suppressAmountInputCallback; private Color defaultSumCostColor = Color.white; private GameObject activeCtasInstance; private string LegacySaveFilePath { get { return Path.Combine(Application.persistentDataPath, "storeSystem_state.json"); } } private void Awake() { if (p_sumCostText != null) { defaultSumCostColor = p_sumCostText.color; } InitializeSortDropdownOptions(); CacheToggles(); RegisterHeaderControlCallbacks(); RegisterToggleCallbacks(); RegisterPurchasePanelCallbacks(); } private void Start() { PlayerEconomyLedger.EnsureInstance().AttachPlayerData(playerData); ExpBottleLedger.EnsureInstance().AttachPlayerData(playerData); StoreOwnershipLedger.EnsureInstance().InitializeIfNeeded(); ResetPriceImageObjects(); LoadFilterPreferences(); SetDefaultToggleState(); LoadPersistedState(); LoadAllStoreItems(); ApplyPersistedStateToItems(); RefreshCurrentView(); } private void OnEnable() { if (cachedItems.Count <= 0) { return; } LoadAllStoreItems(); ApplyPersistedStateToItems(); RefreshCurrentView(); } private void OnDestroy() { CloseActiveCtas(); UnregisterHeaderControlCallbacks(); UnregisterToggleCallbacks(); UnregisterPurchasePanelCallbacks(); } private void CacheToggles() { toggles.Clear(); AddToggle(allItems_toggle); AddToggle(recentlyHot_toggle); AddToggle(idols_toggle); AddToggle(songs_toggle); AddToggle(storyPassage_toggle); AddToggle(consumable_toggle); AddToggle(otherType_toggle); } private void AddToggle(Toggle toggle) { if (toggle != null && !toggles.Contains(toggle)) { toggles.Add(toggle); } } private void RegisterToggleCallbacks() { for (int i = 0; i < toggles.Count; i++) { toggles[i].onValueChanged.AddListener(OnFilterToggleValueChanged); } } private void RegisterHeaderControlCallbacks() { if (closeButton != null) { closeButton.onClick.AddListener(OnCloseButtonClicked); } if (show_only_canPurchase != null) { show_only_canPurchase.onValueChanged.AddListener(OnShowOnlyCanPurchaseChanged); } if (sortDropdown != null) { sortDropdown.onValueChanged.AddListener(OnSortDropdownValueChanged); } } private void UnregisterHeaderControlCallbacks() { if (closeButton != null) { closeButton.onClick.RemoveListener(OnCloseButtonClicked); } if (show_only_canPurchase != null) { show_only_canPurchase.onValueChanged.RemoveListener(OnShowOnlyCanPurchaseChanged); } if (sortDropdown != null) { sortDropdown.onValueChanged.RemoveListener(OnSortDropdownValueChanged); } } private void UnregisterToggleCallbacks() { for (int i = 0; i < toggles.Count; i++) { if (toggles[i] != null) { toggles[i].onValueChanged.RemoveListener(OnFilterToggleValueChanged); } } } private void LoadFilterPreferences() { if (show_only_canPurchase != null) { show_only_canPurchase.SetIsOnWithoutNotify(PlayerPrefs.GetInt(ShowOnlyPurchasablePrefKey, 0) == 1); } if (sortDropdown != null) { int savedValue = PlayerPrefs.GetInt(SortDropdownPrefKey, 0); int safeValue = Mathf.Clamp(savedValue, 0, Mathf.Max(0, sortDropdown.options.Count - 1)); sortDropdown.SetValueWithoutNotify(safeValue); } } private void RegisterPurchasePanelCallbacks() { if (p_iAmount_plus != null) { p_iAmount_plus.onClick.AddListener(OnPurchaseAmountPlusClicked); } if (p_iAmount_minus != null) { p_iAmount_minus.onClick.AddListener(OnPurchaseAmountMinusClicked); } if (p_iAmount_input != null) { p_iAmount_input.onValueChanged.AddListener(OnPurchaseAmountInputValueChanged); } if (purchaseButton != null) { purchaseButton.onClick.AddListener(OnPurchaseButtonClicked); } } private void UnregisterPurchasePanelCallbacks() { if (p_iAmount_plus != null) { p_iAmount_plus.onClick.RemoveListener(OnPurchaseAmountPlusClicked); } if (p_iAmount_minus != null) { p_iAmount_minus.onClick.RemoveListener(OnPurchaseAmountMinusClicked); } if (p_iAmount_input != null) { p_iAmount_input.onValueChanged.RemoveListener(OnPurchaseAmountInputValueChanged); } if (purchaseButton != null) { purchaseButton.onClick.RemoveListener(OnPurchaseButtonClicked); } } private void OnFilterToggleValueChanged(bool _) { currentSelectedItem = null; currentSelectedItemId = -1; currentPurchaseAmount = MinPurchaseAmount; RefreshCurrentView(); } private void OnShowOnlyCanPurchaseChanged(bool isOn) { PlayerPrefs.SetInt(ShowOnlyPurchasablePrefKey, isOn ? 1 : 0); PlayerPrefs.Save(); currentSelectedItem = null; currentSelectedItemId = -1; currentPurchaseAmount = MinPurchaseAmount; RefreshCurrentView(); } private void OnSortDropdownValueChanged(int _) { if (sortDropdown != null) { PlayerPrefs.SetInt(SortDropdownPrefKey, sortDropdown.value); PlayerPrefs.Save(); } RefreshCurrentView(); } private void OnCloseButtonClicked() { CloseActiveCtas(); gameObject.SetActive(false); } private void SetDefaultToggleState() { if (allItems_toggle == null) { return; } for (int i = 0; i < toggles.Count; i++) { if (toggles[i] != null && toggles[i] != allItems_toggle) { toggles[i].SetIsOnWithoutNotify(false); } } allItems_toggle.SetIsOnWithoutNotify(true); } private void LoadAllStoreItems() { cachedItems.Clear(); var uniqueItemIds = new HashSet(); #if UNITY_EDITOR if (!string.IsNullOrEmpty(editorStoreItemPath)) { var guids = UnityEditor.AssetDatabase.FindAssets("t:storeItemSO", new[] { editorStoreItemPath }); for (int i = 0; i < guids.Length; i++) { var assetPath = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]); var itemSO = UnityEditor.AssetDatabase.LoadAssetAtPath(assetPath); TryCacheStoreItem(itemSO, assetPath, uniqueItemIds); } } #endif if (!string.IsNullOrEmpty(runtimeStoreItemPath)) { var runtimeItems = Resources.LoadAll(runtimeStoreItemPath); for (int i = 0; i < runtimeItems.Length; i++) { var itemSO = runtimeItems[i]; var sourceLabel = string.IsNullOrEmpty(itemSO != null ? itemSO.name : string.Empty) ? runtimeStoreItemPath : runtimeStoreItemPath + "/" + itemSO.name; TryCacheStoreItem(itemSO, sourceLabel, uniqueItemIds); } } var playerSkillItems = new List(); PlayerSkillService.AppendDynamicStoreItems(playerSkillItems, coinSprite, materialSprite, memoryBuyPlanSprite, memorySellPlanSprite); for (int i = 0; i < playerSkillItems.Count; i++) { var itemSO = playerSkillItems[i]; var sourceLabel = string.IsNullOrEmpty(itemSO != null ? itemSO.name : string.Empty) ? "playerSkillStore" : "playerSkillStore/" + itemSO.name; TryCacheStoreItem(itemSO, sourceLabel, uniqueItemIds); } } private void InitializeSortDropdownOptions() { if (sortDropdown == null) { return; } sortDropdown.onValueChanged.RemoveListener(OnSortDropdownValueChanged); sortDropdown.ClearOptions(); var options = new List(SortDropdownOptionLabels.Length); for (int i = 0; i < SortDropdownOptionLabels.Length; i++) { options.Add(new Dropdown.OptionData(SortDropdownOptionLabels[i])); } sortDropdown.AddOptions(options); sortDropdown.RefreshShownValue(); } private void TryCacheStoreItem(storeItemSO itemSO, string sourceLabel, HashSet uniqueItemIds) { if (itemSO == null) { return; } if (!uniqueItemIds.Add(itemSO.itemID)) { Debug.LogWarning($"storeSystem: duplicate itemID {itemSO.itemID} found at '{sourceLabel}'. Duplicate item was skipped.", itemSO); return; } cachedItems.Add(itemSO); } private void ApplyPersistedStateToItems() { bool stateChanged = false; for (int i = 0; i < cachedItems.Count; i++) { var itemSO = cachedItems[i]; StoreRuntimeEntry savedEntry; if (!TryGetEntry(itemSO.itemID, out savedEntry)) { #if UNITY_EDITOR if (itemSO.user_has_read || itemSO.purchasedCount > 0) { savedEntry = GetOrCreateEntry(itemSO.itemID); savedEntry.userHasRead = itemSO.user_has_read; savedEntry.purchasedCount = itemSO.purchasedCount; stateChanged = true; } #endif continue; } #if UNITY_EDITOR bool editorHasRead = itemSO.user_has_read; int editorPurchasedCount = itemSO.purchasedCount; if (savedEntry.userHasRead != editorHasRead || savedEntry.purchasedCount != editorPurchasedCount) { savedEntry.userHasRead = editorHasRead; savedEntry.purchasedCount = editorPurchasedCount; stateChanged = true; } #else bool mergedHasRead = itemSO.user_has_read || savedEntry.userHasRead; int mergedPurchasedCount = Mathf.Max(itemSO.purchasedCount, savedEntry.purchasedCount); itemSO.user_has_read = mergedHasRead; itemSO.purchasedCount = mergedPurchasedCount; if (savedEntry.userHasRead != mergedHasRead || savedEntry.purchasedCount != mergedPurchasedCount) { savedEntry.userHasRead = mergedHasRead; savedEntry.purchasedCount = mergedPurchasedCount; stateChanged = true; } #endif } if (stateChanged) { SavePersistedState(); } } public void RefreshCurrentView() { if (itemPrefab == null || contentParent == null) { ClearPurchasePanel(); return; } ClearSpawnedItems(); var visibleItems = new List(); for (int i = 0; i < cachedItems.Count; i++) { var itemSO = cachedItems[i]; if (!ShouldDisplay(itemSO)) { continue; } visibleItems.Add(itemSO); } SortVisibleItems(visibleItems); for (int i = 0; i < visibleItems.Count; i++) { SpawnItem(visibleItems[i]); } ResolveVisibleSelection(visibleItems); } private bool ShouldDisplay(storeItemSO itemSO) { if (itemSO == null) { return false; } if (!itemSO.isOnShelf) { return false; } if (show_only_canPurchase != null && show_only_canPurchase.isOn && !IsCurrentlyPurchasable(itemSO)) { return false; } if (recentlyHot_toggle != null && recentlyHot_toggle.isOn) { return itemSO.isHot; } if (idols_toggle != null && idols_toggle.isOn) { return itemSO.itemType == storeItemSO.ItemType.character; } if (songs_toggle != null && songs_toggle.isOn) { return itemSO.itemType == storeItemSO.ItemType.song; } if (storyPassage_toggle != null && storyPassage_toggle.isOn) { return itemSO.itemType == storeItemSO.ItemType.storyPassage; } if (consumable_toggle != null && consumable_toggle.isOn) { return itemSO.itemType == storeItemSO.ItemType.consumable; } if (otherType_toggle != null && otherType_toggle.isOn) { return itemSO.itemType == storeItemSO.ItemType.otherType; } return allItems_toggle == null || allItems_toggle.isOn; } private void SortVisibleItems(List visibleItems) { if (visibleItems == null || visibleItems.Count <= 1 || sortDropdown == null) { return; } if (sortDropdown.value == 0) { return; } visibleItems.Sort(CompareStoreItems); } private int CompareStoreItems(storeItemSO left, storeItemSO right) { if (left == right) { return 0; } if (left == null) { return 1; } if (right == null) { return -1; } int result; switch (sortDropdown != null ? sortDropdown.value : 0) { case 1: result = ComparePriceAscending(left, right); break; case 2: result = ComparePriceDescending(left, right); break; case 3: result = CompareAscending((int)left.itemType, (int)right.itemType); break; case 4: result = CompareDescending((int)left.itemType, (int)right.itemType); break; case 5: result = CompareQuotaAscending(left, right); break; case 6: result = CompareQuotaDescending(left, right); break; default: result = 0; break; } if (result != 0) { return result; } return CompareAscending(left.itemID, right.itemID); } private static int CompareAscending(int left, int right) { return left.CompareTo(right); } private static int CompareDescending(int left, int right) { return right.CompareTo(left); } private static int CompareQuotaAscending(storeItemSO left, storeItemSO right) { bool leftUnlimited = left.itemPurchaseQuota < 0; bool rightUnlimited = right.itemPurchaseQuota < 0; if (leftUnlimited != rightUnlimited) { return leftUnlimited ? 1 : -1; } return CompareAscending(left.itemPurchaseQuota, right.itemPurchaseQuota); } private static int CompareQuotaDescending(storeItemSO left, storeItemSO right) { bool leftUnlimited = left.itemPurchaseQuota < 0; bool rightUnlimited = right.itemPurchaseQuota < 0; if (leftUnlimited != rightUnlimited) { return leftUnlimited ? -1 : 1; } return CompareDescending(left.itemPurchaseQuota, right.itemPurchaseQuota); } private static int ComparePriceAscending(storeItemSO left, storeItemSO right) { bool leftHasCost = HasSortableCost(left); bool rightHasCost = HasSortableCost(right); if (leftHasCost != rightHasCost) { return leftHasCost ? -1 : 1; } return CompareAscending(GetPrimaryCostAmount(left), GetPrimaryCostAmount(right)); } private static int ComparePriceDescending(storeItemSO left, storeItemSO right) { bool leftHasCost = HasSortableCost(left); bool rightHasCost = HasSortableCost(right); if (leftHasCost != rightHasCost) { return leftHasCost ? -1 : 1; } return CompareDescending(GetPrimaryCostAmount(left), GetPrimaryCostAmount(right)); } private static bool HasSortableCost(storeItemSO itemSO) { return itemSO != null && itemSO.costRequirements != null && itemSO.costRequirements.Count > 0; } private static int GetPrimaryCostAmount(storeItemSO itemSO) { if (!HasSortableCost(itemSO)) { return int.MaxValue; } return itemSO.costRequirements[0].amount; } private bool IsCurrentlyPurchasable(storeItemSO itemSO) { return itemSO != null && itemSO.canbepurchased && !IsAssociatedContentUnlocked(itemSO) && !IsSoldOut(itemSO) && HasValidPurchaseCost(itemSO); } private void SpawnItem(storeItemSO itemSO) { var instance = Instantiate(itemPrefab, contentParent); var itemView = instance.GetComponent(); if (itemView == null) { Debug.LogWarning("storeSystem: itemPrefab missing storeItemPrefab component.", instance); Destroy(instance); return; } itemView.thisItemSO = itemSO; itemView.onItemClicked = HandleItemClicked; itemView.onQuickBuyClicked = HandleQuickBuyClicked; var button = instance.GetComponent