Files
bansonic_beta_main/Assets/storeSystem/storeSystem.cs
T
2026-07-16 23:04:59 +08:00

2040 lines
58 KiB
C#

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<storeItemSO> cachedItems = new List<storeItemSO>();
private readonly List<Toggle> toggles = new List<Toggle>();
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()
{
BindLedgerEvents();
if (cachedItems.Count <= 0)
{
return;
}
LoadAllStoreItems();
ApplyPersistedStateToItems();
RefreshCurrentView();
}
private void OnDisable()
{
UnbindLedgerEvents();
}
private void OnDestroy()
{
UnbindLedgerEvents();
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 BindLedgerEvents()
{
PlayerEconomyLedger.EnsureInstance().OnCoinsChanged -= HandleEconomyChanged;
PlayerEconomyLedger.EnsureInstance().OnCoinsChanged += HandleEconomyChanged;
PlayerEconomyLedger.EnsureInstance().OnMaterialChanged -= HandleMaterialChanged;
PlayerEconomyLedger.EnsureInstance().OnMaterialChanged += HandleMaterialChanged;
ExpBottleLedger.EnsureInstance().OnBottleCountChanged -= HandleBottleCountChanged;
ExpBottleLedger.EnsureInstance().OnBottleCountChanged += HandleBottleCountChanged;
ExpBottleLedger.EnsureInstance().OnLedgerReloaded -= HandleInventoryReloaded;
ExpBottleLedger.EnsureInstance().OnLedgerReloaded += HandleInventoryReloaded;
DushMaterialLedger.EnsureInstance().OnMaterialCountChanged -= HandleGrowthMaterialChanged;
DushMaterialLedger.EnsureInstance().OnMaterialCountChanged += HandleGrowthMaterialChanged;
StoreOwnershipLedger.EnsureInstance().OnOwnershipChanged -= HandleOwnershipChanged;
StoreOwnershipLedger.EnsureInstance().OnOwnershipChanged += HandleOwnershipChanged;
}
private void UnbindLedgerEvents()
{
if (PlayerEconomyLedger.Instance != null)
{
PlayerEconomyLedger.Instance.OnCoinsChanged -= HandleEconomyChanged;
PlayerEconomyLedger.Instance.OnMaterialChanged -= HandleMaterialChanged;
}
if (ExpBottleLedger.Instance != null)
{
ExpBottleLedger.Instance.OnBottleCountChanged -= HandleBottleCountChanged;
ExpBottleLedger.Instance.OnLedgerReloaded -= HandleInventoryReloaded;
}
if (DushMaterialLedger.Instance != null)
{
DushMaterialLedger.Instance.OnMaterialCountChanged -= HandleGrowthMaterialChanged;
}
if (StoreOwnershipLedger.Instance != null)
{
StoreOwnershipLedger.Instance.OnOwnershipChanged -= HandleOwnershipChanged;
}
}
private void HandleEconomyChanged(int _)
{
RefreshVisibleRuntimeState();
}
private void HandleMaterialChanged(int _)
{
RefreshVisibleRuntimeState();
}
private void HandleBottleCountChanged(ExpBottleKind _, int __)
{
RefreshVisibleRuntimeState();
}
private void HandleInventoryReloaded()
{
RefreshVisibleRuntimeState();
}
private void HandleGrowthMaterialChanged(DushMaterialKind _, int __)
{
RefreshVisibleRuntimeState();
}
private void HandleOwnershipChanged(int _)
{
RefreshVisibleRuntimeState();
}
private void RefreshVisibleRuntimeState()
{
if (!isActiveAndEnabled)
{
return;
}
RefreshCurrentView();
}
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<int>();
#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<storeItemSO>(assetPath);
TryCacheStoreItem(itemSO, assetPath, uniqueItemIds);
}
}
#endif
if (!string.IsNullOrEmpty(runtimeStoreItemPath))
{
var runtimeItems = Resources.LoadAll<storeItemSO>(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<storeItemSO>();
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<Dropdown.OptionData>(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<int> 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<storeItemSO>();
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<storeItemSO> 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<storeItemPrefab>();
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<Button>();
if (button != null)
{
button.onClick.AddListener(itemView.NotifyClicked);
}
if (itemView.thisItem_iconImage != null)
{
itemView.thisItem_iconImage.sprite = itemSO.itemIcon;
}
if (itemView.thisItem_nameText != null)
{
itemView.thisItem_nameText.text = itemSO.itemName;
}
itemView.SetDescriptionContent(itemSO.itemName, itemSO.itemDescription);
if (itemView.thisItem_amountAndLimitationText != null)
{
itemView.thisItem_amountAndLimitationText.text = GetAmountAndQuotaText(itemSO);
}
SetupPrice(itemView, itemSO);
SetupAvailabilityState(itemView, itemSO);
SetupReadState(itemView, itemSO);
SetupQuickBuyState(itemView, itemSO);
}
private void ResolveVisibleSelection(List<storeItemSO> visibleItems)
{
if (visibleItems == null || visibleItems.Count == 0)
{
currentSelectedItem = null;
currentSelectedItemId = -1;
currentPurchaseAmount = MinPurchaseAmount;
ClearPurchasePanel();
return;
}
storeItemSO itemToSelect = null;
if (currentSelectedItemId >= 0)
{
for (int i = 0; i < visibleItems.Count; i++)
{
if (visibleItems[i].itemID == currentSelectedItemId)
{
itemToSelect = visibleItems[i];
break;
}
}
}
bool resetAmount = false;
if (itemToSelect == null)
{
itemToSelect = visibleItems[0];
resetAmount = true;
}
ApplySelectedItem(itemToSelect, resetAmount);
}
private void ApplySelectedItem(storeItemSO itemSO, bool resetAmount)
{
currentSelectedItem = itemSO;
currentSelectedItemId = itemSO != null ? itemSO.itemID : -1;
if (itemSO == null)
{
ClearPurchasePanel();
return;
}
if (p_itemName != null)
{
p_itemName.text = GetDisplayItemName(itemSO.itemName);
ApplyItemNameColorState(p_itemName);
}
ApplyItemRarityBottomState(itemSO);
if (p_itemImage != null)
{
p_itemImage.sprite = itemSO.itemIcon;
p_itemImage.enabled = itemSO.itemIcon != null;
}
if (p_detailedDescriptionText != null)
{
p_detailedDescriptionText.text = string.IsNullOrWhiteSpace(itemSO.itemDetailedDescription)
? "暂无描述信息"
: itemSO.itemDetailedDescription;
}
if (p_itemUsageText != null)
{
p_itemUsageText.text = ItemUsageTagUtility.ResolveDisplayName(itemSO);
}
if (resetAmount || IsSelectableEquipmentRewardItem(itemSO))
{
SetPurchaseAmount(MinPurchaseAmount, false);
}
else
{
UpdatePurchaseAmountInput();
RefreshPurchaseSummary();
}
UpdatePurchaseAmountControlsState(itemSO);
RefreshPurchaseButtonState();
}
private void ClearPurchasePanel()
{
if (p_itemName != null)
{
p_itemName.text = string.Empty;
ApplyItemNameColorState(p_itemName);
}
if (p_itemImage != null)
{
p_itemImage.sprite = null;
p_itemImage.enabled = false;
}
if (p_detailedDescriptionText != null)
{
p_detailedDescriptionText.text = string.Empty;
}
if (p_itemUsageText != null)
{
p_itemUsageText.text = string.Empty;
}
ApplyItemRarityBottomState(null);
if (p_sumCostText != null)
{
p_sumCostText.text = string.Empty;
p_sumCostText.color = defaultSumCostColor;
}
SetPriceImageState(p_sumCostImage, null, false);
currentPurchaseAmount = MinPurchaseAmount;
UpdatePurchaseAmountInput();
UpdatePurchaseAmountControlsState(null);
RefreshPurchaseButtonState();
}
private void OnPurchaseAmountPlusClicked()
{
SetPurchaseAmount(currentPurchaseAmount + 1, true);
}
private void OnPurchaseAmountMinusClicked()
{
SetPurchaseAmount(currentPurchaseAmount - 1, true);
}
private void OnPurchaseAmountInputValueChanged(string value)
{
if (suppressAmountInputCallback)
{
return;
}
if (string.IsNullOrWhiteSpace(value))
{
currentPurchaseAmount = MinPurchaseAmount;
RefreshPurchaseSummary();
return;
}
long parsedValue;
if (!long.TryParse((value ?? string.Empty).Trim(), out parsedValue))
{
parsedValue = MinPurchaseAmount;
}
int safeValue;
if (parsedValue > int.MaxValue)
{
safeValue = int.MaxValue;
}
else if (parsedValue < int.MinValue)
{
safeValue = int.MinValue;
}
else
{
safeValue = (int)parsedValue;
}
bool reachedUpperLimit = safeValue > MaxPurchaseAmount;
bool reachedLowerLimit = safeValue < MinPurchaseAmount;
int clampedValue = Mathf.Clamp(safeValue, MinPurchaseAmount, MaxPurchaseAmount);
currentPurchaseAmount = clampedValue;
RefreshPurchaseSummary();
if (reachedUpperLimit)
{
gNotice.warning.display("达到单次购买限额");
UpdatePurchaseAmountInput();
}
else if (reachedLowerLimit)
{
gNotice.warning.display("本店禁止无实物交易");
UpdatePurchaseAmountInput();
}
}
private void UpdatePurchaseAmountControlsState(storeItemSO itemSO)
{
bool forceSinglePurchase = IsSelectableEquipmentRewardItem(itemSO);
if (p_iAmount_plus != null)
{
p_iAmount_plus.interactable = !forceSinglePurchase;
}
if (p_iAmount_minus != null)
{
p_iAmount_minus.interactable = !forceSinglePurchase;
}
if (p_iAmount_input != null)
{
p_iAmount_input.interactable = !forceSinglePurchase;
}
}
private void SetPurchaseAmount(int amount, bool showWarnings)
{
int clampedAmount = amount;
if (amount > MaxPurchaseAmount)
{
clampedAmount = MaxPurchaseAmount;
if (showWarnings)
{
gNotice.warning.display("达到单次购买限额");
}
}
else if (amount < MinPurchaseAmount)
{
clampedAmount = MinPurchaseAmount;
if (showWarnings)
{
gNotice.warning.display("本店禁止无实物交易");
}
}
currentPurchaseAmount = clampedAmount;
UpdatePurchaseAmountInput();
RefreshPurchaseSummary();
}
private void UpdatePurchaseAmountInput()
{
if (p_iAmount_input == null)
{
return;
}
suppressAmountInputCallback = true;
p_iAmount_input.text = currentPurchaseAmount.ToString();
suppressAmountInputCallback = false;
}
private void RefreshPurchaseSummary()
{
ResetPriceImageObjectState(p_sumCostImage);
if (currentSelectedItem == null)
{
if (p_sumCostText != null)
{
p_sumCostText.text = string.Empty;
p_sumCostText.color = defaultSumCostColor;
}
SetPriceImageState(p_sumCostImage, null, false);
RefreshPurchaseButtonState();
return;
}
if (!CanPurchaseCurrentSelection(currentSelectedItem))
{
if (p_sumCostText != null)
{
p_sumCostText.text = "不可购买";
p_sumCostText.color = defaultSumCostColor;
}
SetCannotBuyImageState(p_sumCostImage);
RefreshPurchaseButtonState();
return;
}
var cost = currentSelectedItem.costRequirements[0];
long totalCost = (long)cost.amount * currentPurchaseAmount;
if (p_sumCostText != null)
{
p_sumCostText.text = totalCost.ToString();
p_sumCostText.color = CanAffordCurrentSelection(totalCost) ? defaultSumCostColor : InsufficientCostColor;
}
SetPriceImageState(p_sumCostImage, GetCurrencySprite(cost.currencyType), true);
RefreshPurchaseButtonState();
}
private Sprite GetCurrencySprite(storeItemSO.CurrencyType currencyType)
{
if (currencyType == storeItemSO.CurrencyType.coins)
{
return coinSprite;
}
if (currencyType == storeItemSO.CurrencyType.material)
{
return materialSprite;
}
return null;
}
private bool HasValidPurchaseCost(storeItemSO itemSO)
{
if (itemSO == null || itemSO.costRequirements == null || itemSO.costRequirements.Count == 0)
{
return false;
}
return itemSO.costRequirements[0].currencyType == storeItemSO.CurrencyType.coins
|| itemSO.costRequirements[0].currencyType == storeItemSO.CurrencyType.material;
}
private void SetPriceImageState(Image priceImage, Sprite sprite, bool shouldShow)
{
if (priceImage == null)
{
return;
}
priceImage.gameObject.SetActive(shouldShow);
if (!shouldShow)
{
priceImage.sprite = null;
priceImage.enabled = false;
return;
}
priceImage.sprite = sprite;
priceImage.enabled = sprite != null;
}
private void SetCannotBuyImageState(Image priceImage)
{
if (priceImage == null)
{
return;
}
if (cannotBuySprite == null)
{
SetPriceImageState(priceImage, null, false);
return;
}
SetPriceImageState(priceImage, cannotBuySprite, true);
}
private void ResetPriceImageObjects()
{
ResetPriceImageObjectState(p_sumCostImage);
}
private void ResetPriceImageObjectState(Image priceImage)
{
if (priceImage == null)
{
return;
}
priceImage.gameObject.SetActive(true);
priceImage.sprite = null;
priceImage.enabled = false;
}
private void RefreshPurchaseButtonState()
{
if (purchaseButton == null)
{
return;
}
purchaseButton.interactable = CanPurchaseCurrentSelection(currentSelectedItem);
}
private void OnPurchaseButtonClicked()
{
if (currentSelectedItem == null)
{
return;
}
ExecutePurchase(currentSelectedItem, currentPurchaseAmount, true);
}
private void ExecutePurchase(storeItemSO itemSO, int purchaseAmount, bool refreshSummaryOnFailure)
{
if (itemSO == null)
{
return;
}
if (PlayerSkillService.IsPlayerSkillStoreItem(itemSO))
{
HandlePlayerSkillStorePurchaseClicked(itemSO, purchaseAmount, refreshSummaryOnFailure);
return;
}
if (IsSelectableEquipmentRewardItem(itemSO))
{
HandleSelectableEquipmentPurchaseClicked(itemSO, purchaseAmount, refreshSummaryOnFailure);
return;
}
string failureMessage;
int grantedCount;
if (!StoreExpBottlePurchaseService.TryPurchase(playerData, itemSO, purchaseAmount, out failureMessage, out grantedCount))
{
if (!string.IsNullOrEmpty(failureMessage))
{
gNotice.warning.display(failureMessage);
}
if (refreshSummaryOnFailure)
{
RefreshPurchaseSummary();
}
return;
}
RegisterPurchasedCount(itemSO.itemID, grantedCount);
gItemGet.display(gItemGet.FromStoreItem(itemSO, grantedCount));
RefreshCurrentView();
}
private void HandlePlayerSkillStorePurchaseClicked(storeItemSO itemSO)
{
HandlePlayerSkillStorePurchaseClicked(itemSO, currentPurchaseAmount, true);
}
private void HandlePlayerSkillStorePurchaseClicked(storeItemSO itemSO, int purchaseAmount, bool refreshSummaryOnFailure)
{
if (itemSO == null)
{
return;
}
string failureMessage;
gItemGet.ItemEntry rewardEntry;
if (!PlayerSkillService.TryPurchasePlayerSkillStoreItem(itemSO, purchaseAmount, out failureMessage, out rewardEntry))
{
if (!string.IsNullOrEmpty(failureMessage))
{
gNotice.warning.display(failureMessage);
}
if (refreshSummaryOnFailure)
{
RefreshPurchaseSummary();
}
return;
}
RegisterPurchasedCount(itemSO.itemID, purchaseAmount);
gItemGet.display(rewardEntry);
RefreshCurrentView();
}
private void HandleSelectableEquipmentPurchaseClicked(storeItemSO itemSO)
{
HandleSelectableEquipmentPurchaseClicked(itemSO, currentPurchaseAmount, true);
}
private void HandleSelectableEquipmentPurchaseClicked(storeItemSO itemSO, int purchaseAmount, bool refreshSummaryOnFailure)
{
if (itemSO == null)
{
return;
}
if (purchaseAmount != 1)
{
gNotice.warning.display("该物品仅支持单份购买");
if (refreshSummaryOnFailure)
{
SetPurchaseAmount(1, false);
}
return;
}
if (!TryGetSelectableEquipmentRequirement(itemSO, out smeltStageRewardSO rewardSource, out smeltStageRewardSO.SmeltStageRewardRequirement requirement))
{
gNotice.warning.display("当前未配置发放逻辑");
return;
}
if (itemSO.costRequirements == null || itemSO.costRequirements.Count == 0)
{
gNotice.warning.display("商品数据丢失");
return;
}
var primaryCost = itemSO.costRequirements[0];
int totalCost = Mathf.Max(0, primaryCost.amount);
string insufficientMessage = primaryCost.currencyType == storeItemSO.CurrencyType.material ? "记忆碎片不足" : "算力不足";
if (!HasEnoughCurrency(primaryCost.currencyType, totalCost))
{
gNotice.warning.display(insufficientMessage);
if (refreshSummaryOnFailure)
{
RefreshPurchaseSummary();
}
return;
}
bool requireTypeSelection = RequiresTypeSelection(requirement);
bool requireSkillSelection = RequiresSkillSelection(requirement);
if (!requireTypeSelection && !requireSkillSelection)
{
TryCompleteSelectableEquipmentPurchase(itemSO, rewardSource, requirement, null, 0, refreshSummaryOnFailure);
return;
}
if (ctasPrefab == null || ctasParent == null || rewardSource == null || rewardSource.rewardRandomConfig == null)
{
gNotice.warning.display("选择器未配置");
return;
}
CloseActiveCtas();
GameObject instance = Instantiate(ctasPrefab, ctasParent);
activeCtasInstance = instance;
global::ctasPrefab selector = instance.GetComponent<global::ctasPrefab>();
if (selector == null)
{
Destroy(instance);
activeCtasInstance = null;
gNotice.warning.display("选择器未配置");
return;
}
equipmentSO selectionTemplate = rewardSource.rewardTemplate != null
? Instantiate(rewardSource.rewardTemplate)
: ScriptableObject.CreateInstance<equipmentSO>();
selectionTemplate.hideFlags = HideFlags.DontSave;
selectionTemplate.sa_skillID = 0;
selectionTemplate.ia_skillID = 0;
selectionTemplate.skillType = requirement.chosenSkillType;
selector.Initialize(
selectionTemplate,
rewardSource.rewardRandomConfig,
requireTypeSelection,
requireSkillSelection,
(selectedType, selectedSkillGroupId) =>
{
activeCtasInstance = null;
TryCompleteSelectableEquipmentPurchase(itemSO, rewardSource, requirement, selectedType, selectedSkillGroupId, refreshSummaryOnFailure);
},
() => { activeCtasInstance = null; });
}
private void TryCompleteSelectableEquipmentPurchase(
storeItemSO itemSO,
smeltStageRewardSO rewardSource,
smeltStageRewardSO.SmeltStageRewardRequirement requirement,
equipmentSO.EquipmentSkillType? selectedType,
int selectedSkillGroupId,
bool refreshSummaryOnFailure)
{
if (itemSO == null || rewardSource == null || requirement == null || itemSO.costRequirements == null || itemSO.costRequirements.Count == 0)
{
gNotice.warning.display("商品数据丢失");
return;
}
var primaryCost = itemSO.costRequirements[0];
int totalCost = Mathf.Max(0, primaryCost.amount);
string insufficientMessage = primaryCost.currencyType == storeItemSO.CurrencyType.material ? "记忆碎片不足" : "算力不足";
if (!HasEnoughCurrency(primaryCost.currencyType, totalCost))
{
gNotice.warning.display(insufficientMessage);
if (refreshSummaryOnFailure)
{
RefreshPurchaseSummary();
}
return;
}
if (!TrySpendCurrency(primaryCost.currencyType, totalCost))
{
gNotice.warning.display(insufficientMessage);
if (refreshSummaryOnFailure)
{
RefreshPurchaseSummary();
}
return;
}
equipmentSO generated = equipmentGenerator.GenerateFromSmeltReward(
rewardSource,
requirement,
selectedType,
selectedSkillGroupId);
if (generated == null)
{
RefundCurrency(primaryCost.currencyType, totalCost);
gNotice.warning.display("装备发放失败");
if (refreshSummaryOnFailure)
{
RefreshPurchaseSummary();
}
return;
}
RegisterPurchasedCount(itemSO.itemID, 1);
gItemGet.display(gItemGet.FromEquipment(generated, 1));
RefreshAllEquipBags();
}
private string GetAmountAndQuotaText(storeItemSO itemSO)
{
string prefix = itemSO.itemSinglePurchaseQty == 1 ? string.Empty : $"{itemSO.itemSinglePurchaseQty}个装/";
if (itemSO.itemPurchaseQuota < 0)
{
return prefix + "不限购";
}
return $"{prefix}限购{itemSO.purchasedCount}/{itemSO.itemPurchaseQuota}个";
}
private void SetupPrice(storeItemPrefab itemView, storeItemSO itemSO)
{
if (itemView.thisItem_priceText != null)
{
itemView.thisItem_priceText.text = GetPriceAmountText(itemSO);
}
if (itemView.thisPrice_iconImage == null)
{
return;
}
ResetPriceImageObjectState(itemView.thisPrice_iconImage);
if (!ShouldShowPriceIcon(itemSO))
{
SetCannotBuyImageState(itemView.thisPrice_iconImage);
return;
}
var cost = itemSO.costRequirements[0];
SetPriceImageState(itemView.thisPrice_iconImage, GetCurrencySprite(cost.currencyType), true);
}
private string GetPriceAmountText(storeItemSO itemSO)
{
if (itemSO == null || !itemSO.canbepurchased || IsAssociatedContentUnlocked(itemSO) || !HasValidPurchaseCost(itemSO))
{
return "不可购买";
}
return itemSO.costRequirements[0].amount.ToString();
}
private bool ShouldShowPriceIcon(storeItemSO itemSO)
{
return itemSO != null
&& itemSO.canbepurchased
&& !IsAssociatedContentUnlocked(itemSO)
&& HasValidPurchaseCost(itemSO)
&& GetCurrencySprite(itemSO.costRequirements[0].currencyType) != null;
}
private bool CanPurchaseCurrentSelection(storeItemSO itemSO)
{
return CanPurchaseAmount(itemSO, currentPurchaseAmount);
}
private bool CanPurchaseAmount(storeItemSO itemSO, int purchaseAmount)
{
return IsCurrentlyPurchasable(itemSO)
&& purchaseAmount >= MinPurchaseAmount
&& (!IsSelectableEquipmentRewardItem(itemSO) || purchaseAmount == 1);
}
private void ApplyItemNameColorState(Text itemNameText)
{
if (itemNameText == null)
{
return;
}
itemNameText.supportRichText = p_itemNameFollowRichTextColor;
if (!p_itemNameFollowRichTextColor)
{
itemNameText.color = Color.white;
}
}
private string GetDisplayItemName(string rawItemName)
{
if (string.IsNullOrEmpty(rawItemName))
{
return string.Empty;
}
if (p_itemNameFollowRichTextColor)
{
return rawItemName;
}
return RichTextTagRegex.Replace(rawItemName, string.Empty);
}
private void ApplyItemRarityBottomState(storeItemSO itemSO)
{
if (p_itemRarityBtmImage == null)
{
return;
}
Sprite sprite = ResolveRarityBottomSprite(itemSO != null ? itemSO.itemRarity : ItemRarity.None);
p_itemRarityBtmImage.sprite = sprite;
p_itemRarityBtmImage.enabled = sprite != null;
}
private Sprite ResolveRarityBottomSprite(ItemRarity rarity)
{
if (raritybtmImageSprite != null)
{
int enumIndex = (int)rarity;
if (enumIndex >= 0 && enumIndex < raritybtmImageSprite.Length)
{
Sprite directSprite = raritybtmImageSprite[enumIndex];
if (directSprite != null)
{
return directSprite;
}
}
if (rarity != ItemRarity.None)
{
int compactIndex = (int)rarity - 1;
if (compactIndex >= 0 && compactIndex < raritybtmImageSprite.Length)
{
Sprite compactSprite = raritybtmImageSprite[compactIndex];
if (compactSprite != null)
{
return compactSprite;
}
}
}
}
return null;
}
private bool CanAffordCurrentSelection(long totalCost)
{
if (currentSelectedItem == null || totalCost <= 0)
{
return true;
}
var cost = currentSelectedItem.costRequirements != null && currentSelectedItem.costRequirements.Count > 0
? currentSelectedItem.costRequirements[0]
: null;
if (cost == null)
{
return false;
}
switch (cost.currencyType)
{
case storeItemSO.CurrencyType.coins:
return PlayerEconomyLedger.EnsureInstance().HasEnoughCoins(totalCost);
case storeItemSO.CurrencyType.material:
return PlayerEconomyLedger.EnsureInstance().HasEnoughMaterial(totalCost);
default:
return false;
}
}
private bool IsSelectableEquipmentRewardItem(storeItemSO itemSO)
{
return itemSO != null
&& itemSO.associatedSelectableEquipmentRewardSource != null
&& itemSO.associatedSelectableEquipmentRewardIndex >= 0;
}
private bool TryGetSelectableEquipmentRequirement(
storeItemSO itemSO,
out smeltStageRewardSO rewardSource,
out smeltStageRewardSO.SmeltStageRewardRequirement requirement)
{
rewardSource = null;
requirement = null;
if (!IsSelectableEquipmentRewardItem(itemSO))
{
return false;
}
rewardSource = itemSO.associatedSelectableEquipmentRewardSource;
if (rewardSource.rewardRequirements == null
|| itemSO.associatedSelectableEquipmentRewardIndex >= rewardSource.rewardRequirements.Length)
{
rewardSource = null;
return false;
}
requirement = rewardSource.rewardRequirements[itemSO.associatedSelectableEquipmentRewardIndex];
return requirement != null;
}
private bool RequiresTypeSelection(smeltStageRewardSO.SmeltStageRewardRequirement requirement)
{
if (requirement == null)
{
return false;
}
if (requirement.equipmentType == smeltStageRewardSO.equipType.selfChosenType)
{
return true;
}
return !string.IsNullOrEmpty(requirement.equipRewardName)
&& requirement.equipRewardName.Contains("自选");
}
private bool RequiresSkillSelection(smeltStageRewardSO.SmeltStageRewardRequirement requirement)
{
return requirement != null
&& requirement.skillRequirement == smeltStageRewardSO.skillOwned.selfChosen;
}
private bool HasEnoughCurrency(storeItemSO.CurrencyType currencyType, int amount)
{
switch (currencyType)
{
case storeItemSO.CurrencyType.coins:
return PlayerEconomyLedger.EnsureInstance().HasEnoughCoins(amount);
case storeItemSO.CurrencyType.material:
return PlayerEconomyLedger.EnsureInstance().HasEnoughMaterial(amount);
default:
return false;
}
}
private bool TrySpendCurrency(storeItemSO.CurrencyType currencyType, int amount)
{
switch (currencyType)
{
case storeItemSO.CurrencyType.coins:
return PlayerEconomyLedger.EnsureInstance().TrySpendCoins(amount);
case storeItemSO.CurrencyType.material:
return PlayerEconomyLedger.EnsureInstance().TrySpendMaterial(amount);
default:
return false;
}
}
private void RefundCurrency(storeItemSO.CurrencyType currencyType, int amount)
{
switch (currencyType)
{
case storeItemSO.CurrencyType.coins:
PlayerEconomyLedger.EnsureInstance().AddCoins(amount);
break;
case storeItemSO.CurrencyType.material:
PlayerEconomyLedger.EnsureInstance().AddMaterial(amount);
break;
}
}
private void CloseActiveCtas()
{
if (activeCtasInstance != null)
{
Destroy(activeCtasInstance);
activeCtasInstance = null;
}
}
private static void RefreshAllEquipBags()
{
equipBag[] bags = Resources.FindObjectsOfTypeAll<equipBag>();
for (int i = 0; i < bags.Length; i++)
{
equipBag bag = bags[i];
if (bag == null)
{
continue;
}
bag.Rebuild();
}
}
private void SetupAvailabilityState(storeItemPrefab itemView, storeItemSO itemSO)
{
bool isSoldOut = IsSoldOut(itemSO);
bool alreadyUnlocked = IsAssociatedContentUnlocked(itemSO);
bool cannotPurchase = !itemSO.canbepurchased;
if (itemView.leftCorner_statusImage != null)
{
itemView.leftCorner_statusImage.SetActive(isSoldOut);
}
if (itemView.lock_cannotClickImage != null)
{
itemView.lock_cannotClickImage.SetActive(isSoldOut || cannotPurchase || alreadyUnlocked);
}
if (itemView.why_cannot_buy != null)
{
if (isSoldOut)
{
itemView.why_cannot_buy.text = "已售罄";
}
else if (alreadyUnlocked)
{
itemView.why_cannot_buy.text = "已解锁";
}
else if (cannotPurchase)
{
itemView.why_cannot_buy.text = "物品未上架";
}
else
{
itemView.why_cannot_buy.text = string.Empty;
}
}
}
private void SetupReadState(storeItemPrefab itemView, storeItemSO itemSO)
{
if (itemView.rightCorner_statusImage != null)
{
itemView.rightCorner_statusImage.SetActive(!itemSO.user_has_read);
}
}
private void SetupQuickBuyState(storeItemPrefab itemView, storeItemSO itemSO)
{
if (itemView == null)
{
return;
}
itemView.RefreshQuickBuyState(itemSO != null && CanPurchaseAmount(itemSO, 1));
}
private bool IsSoldOut(storeItemSO itemSO)
{
if (itemSO == null || itemSO.itemPurchaseQuota < 0)
{
return false;
}
return itemSO.purchasedCount >= itemSO.itemPurchaseQuota;
}
private bool IsAssociatedContentUnlocked(storeItemSO itemSO)
{
return StoreOwnershipLedger.EnsureInstance().IsOwned(itemSO);
}
private void HandleItemClicked(storeItemPrefab itemView)
{
if (itemView == null || itemView.thisItemSO == null)
{
return;
}
currentSelectedItemId = itemView.thisItemSO.itemID;
ApplySelectedItem(itemView.thisItemSO, true);
if (!itemView.thisItemSO.user_has_read)
{
itemView.thisItemSO.user_has_read = true;
var entry = GetOrCreateEntry(itemView.thisItemSO.itemID);
entry.userHasRead = true;
entry.purchasedCount = itemView.thisItemSO.purchasedCount;
SavePersistedState();
RefreshCurrentView();
}
}
private void HandleQuickBuyClicked(storeItemPrefab itemView)
{
if (itemView == null || itemView.thisItemSO == null)
{
return;
}
ExecutePurchase(itemView.thisItemSO, 1, false);
}
public void RegisterPurchasedCount(int itemID, int purchasedAmount)
{
if (purchasedAmount <= 0)
{
return;
}
for (int i = 0; i < cachedItems.Count; i++)
{
var itemSO = cachedItems[i];
if (itemSO.itemID != itemID)
{
continue;
}
itemSO.purchasedCount += purchasedAmount;
var entry = GetOrCreateEntry(itemID);
entry.userHasRead = itemSO.user_has_read;
entry.purchasedCount = itemSO.purchasedCount;
SavePersistedState();
RefreshCurrentView();
return;
}
}
public void RefreshPlayerSkillItems()
{
if (!isActiveAndEnabled)
{
return;
}
LoadAllStoreItems();
ApplyPersistedStateToItems();
RefreshCurrentView();
}
private bool TryGetEntry(int itemID, out StoreRuntimeEntry entry)
{
entry = null;
if (persistedState.entries == null)
{
persistedState.entries = new List<StoreRuntimeEntry>();
return false;
}
for (int i = 0; i < persistedState.entries.Count; i++)
{
if (persistedState.entries[i] != null && persistedState.entries[i].itemID == itemID)
{
entry = persistedState.entries[i];
return true;
}
}
return false;
}
private StoreRuntimeEntry GetOrCreateEntry(int itemID)
{
StoreRuntimeEntry entry;
if (TryGetEntry(itemID, out entry))
{
return entry;
}
if (persistedState.entries == null)
{
persistedState.entries = new List<StoreRuntimeEntry>();
}
entry = new StoreRuntimeEntry
{
itemID = itemID,
userHasRead = false,
purchasedCount = 0
};
persistedState.entries.Add(entry);
return entry;
}
private void LoadPersistedState()
{
persistedState = new StoreRuntimeSaveData();
try
{
StoreRuntimeSaveData loadedData;
if (!SecureSaveVault.TryLoadJson(StoreStateSaveCategory, StoreStateSaveKey, out loadedData, LegacySaveFilePath))
{
return;
}
if (loadedData != null && loadedData.entries != null)
{
persistedState = loadedData;
NormalizePersistedEntries();
}
}
catch (Exception ex)
{
Debug.LogWarning($"storeSystem: failed to load persisted state. {ex.Message}");
persistedState = new StoreRuntimeSaveData();
}
}
private void NormalizePersistedEntries()
{
if (persistedState.entries == null)
{
persistedState.entries = new List<StoreRuntimeEntry>();
return;
}
var normalizedEntries = new List<StoreRuntimeEntry>();
var indexByItemId = new Dictionary<int, int>();
bool hadDuplicates = false;
for (int i = 0; i < persistedState.entries.Count; i++)
{
var source = persistedState.entries[i];
if (source == null)
{
continue;
}
int existingIndex;
if (indexByItemId.TryGetValue(source.itemID, out existingIndex))
{
var target = normalizedEntries[existingIndex];
target.userHasRead = target.userHasRead || source.userHasRead;
target.purchasedCount = Mathf.Max(target.purchasedCount, source.purchasedCount);
hadDuplicates = true;
continue;
}
var clone = new StoreRuntimeEntry
{
itemID = source.itemID,
userHasRead = source.userHasRead,
purchasedCount = source.purchasedCount
};
indexByItemId.Add(clone.itemID, normalizedEntries.Count);
normalizedEntries.Add(clone);
}
persistedState.entries = normalizedEntries;
if (hadDuplicates)
{
SavePersistedState();
}
}
private void SavePersistedState()
{
try
{
NormalizePersistedEntries();
SecureSaveVault.SaveJson(StoreStateSaveCategory, StoreStateSaveKey, persistedState, LegacySaveFilePath);
#if UNITY_EDITOR
PersistStateBackToEditorAssets();
#endif
}
catch (Exception ex)
{
Debug.LogWarning($"storeSystem: failed to save persisted state. {ex.Message}");
}
}
#if UNITY_EDITOR
private void PersistStateBackToEditorAssets()
{
for (int i = 0; i < cachedItems.Count; i++)
{
UnityEditor.EditorUtility.SetDirty(cachedItems[i]);
}
}
#endif
private void ClearSpawnedItems()
{
for (int i = contentParent.childCount - 1; i >= 0; i--)
{
Destroy(contentParent.GetChild(i).gameObject);
}
}
[Serializable]
private class StoreRuntimeSaveData
{
public List<StoreRuntimeEntry> entries = new List<StoreRuntimeEntry>();
}
[Serializable]
private class StoreRuntimeEntry
{
public int itemID;
public bool userHasRead;
public int purchasedCount;
}
}