1354 lines
37 KiB
C#
1354 lines
37 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using Bansonic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
public class storeSystem : MonoBehaviour
|
|
{
|
|
private const string ShowOnlyPurchasablePrefKey = "store.show_only_can_purchase";
|
|
private const string SortDropdownPrefKey = "store.sort_dropdown_value";
|
|
[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 cannotBuySprite;
|
|
|
|
[Header("selecting item purchase")]
|
|
public Text p_itemName;
|
|
public Image p_itemImage;
|
|
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_detailedDescriptionText;
|
|
|
|
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 string SaveFilePath
|
|
{
|
|
get { return Path.Combine(Application.persistentDataPath, "storeSystem_state.json"); }
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
if (p_sumCostText != null)
|
|
{
|
|
defaultSumCostColor = p_sumCostText.color;
|
|
}
|
|
|
|
CacheToggles();
|
|
RegisterHeaderControlCallbacks();
|
|
RegisterToggleCallbacks();
|
|
RegisterPurchasePanelCallbacks();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
PlayerEconomyLedger.EnsureInstance().AttachPlayerData(playerData);
|
|
ExpBottleLedger.EnsureInstance().AttachPlayerData(playerData);
|
|
ResetPriceImageObjects();
|
|
LoadFilterPreferences();
|
|
SetDefaultToggleState();
|
|
LoadPersistedState();
|
|
LoadAllStoreItems();
|
|
ApplyPersistedStateToItems();
|
|
RefreshCurrentView();
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
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()
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
&& !IsSoldOut(itemSO)
|
|
&& HasCoinRequirement(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;
|
|
|
|
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);
|
|
}
|
|
|
|
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 = itemSO.itemName ?? string.Empty;
|
|
}
|
|
|
|
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 (resetAmount)
|
|
{
|
|
SetPurchaseAmount(MinPurchaseAmount, false);
|
|
}
|
|
else
|
|
{
|
|
UpdatePurchaseAmountInput();
|
|
RefreshPurchaseSummary();
|
|
}
|
|
|
|
RefreshPurchaseButtonState();
|
|
}
|
|
|
|
private void ClearPurchasePanel()
|
|
{
|
|
if (p_itemName != null)
|
|
{
|
|
p_itemName.text = string.Empty;
|
|
}
|
|
|
|
if (p_itemImage != null)
|
|
{
|
|
p_itemImage.sprite = null;
|
|
p_itemImage.enabled = false;
|
|
}
|
|
|
|
if (p_detailedDescriptionText != null)
|
|
{
|
|
p_detailedDescriptionText.text = string.Empty;
|
|
}
|
|
|
|
if (p_sumCostText != null)
|
|
{
|
|
p_sumCostText.text = string.Empty;
|
|
p_sumCostText.color = defaultSumCostColor;
|
|
}
|
|
|
|
SetPriceImageState(p_sumCostImage, null, false);
|
|
|
|
currentPurchaseAmount = MinPurchaseAmount;
|
|
UpdatePurchaseAmountInput();
|
|
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 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;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private bool HasCoinRequirement(storeItemSO itemSO)
|
|
{
|
|
if (itemSO == null || itemSO.costRequirements == null || itemSO.costRequirements.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return GetCurrencySprite(itemSO.costRequirements[0].currencyType) != null;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
string failureMessage;
|
|
int grantedCount;
|
|
if (!StoreExpBottlePurchaseService.TryPurchase(playerData, currentSelectedItem, currentPurchaseAmount, out failureMessage, out grantedCount))
|
|
{
|
|
if (!string.IsNullOrEmpty(failureMessage))
|
|
{
|
|
gNotice.warning.display(failureMessage);
|
|
}
|
|
|
|
RefreshPurchaseSummary();
|
|
return;
|
|
}
|
|
|
|
RegisterPurchasedCount(currentSelectedItem.itemID, grantedCount);
|
|
RefreshPurchaseSummary();
|
|
}
|
|
|
|
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 || !HasCoinRequirement(itemSO))
|
|
{
|
|
return "不可购买";
|
|
}
|
|
|
|
return itemSO.costRequirements[0].amount.ToString();
|
|
}
|
|
|
|
private bool ShouldShowPriceIcon(storeItemSO itemSO)
|
|
{
|
|
return itemSO != null && itemSO.canbepurchased && HasCoinRequirement(itemSO);
|
|
}
|
|
|
|
private bool CanPurchaseCurrentSelection(storeItemSO itemSO)
|
|
{
|
|
return IsCurrentlyPurchasable(itemSO);
|
|
}
|
|
|
|
private bool CanAffordCurrentSelection(long totalCost)
|
|
{
|
|
if (totalCost <= 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return PlayerEconomyLedger.EnsureInstance().HasEnoughCoins(totalCost);
|
|
}
|
|
|
|
private void SetupAvailabilityState(storeItemPrefab itemView, storeItemSO itemSO)
|
|
{
|
|
bool isSoldOut = IsSoldOut(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);
|
|
}
|
|
|
|
if (itemView.why_cannot_buy != null)
|
|
{
|
|
if (isSoldOut)
|
|
{
|
|
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 bool IsSoldOut(storeItemSO itemSO)
|
|
{
|
|
if (itemSO == null || itemSO.itemPurchaseQuota < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return itemSO.purchasedCount >= itemSO.itemPurchaseQuota;
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
if (!File.Exists(SaveFilePath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var json = File.ReadAllText(SaveFilePath);
|
|
var loadedData = JsonUtility.FromJson<StoreRuntimeSaveData>(json);
|
|
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();
|
|
var json = JsonUtility.ToJson(persistedState, true);
|
|
File.WriteAllText(SaveFilePath, json);
|
|
#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;
|
|
}
|
|
}
|