1763 lines
50 KiB
C#
1763 lines
50 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";
|
|
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;
|
|
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_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;
|
|
}
|
|
|
|
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<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 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;
|
|
|
|
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 (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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (PlayerSkillService.IsPlayerSkillStoreItem(currentSelectedItem))
|
|
{
|
|
HandlePlayerSkillStorePurchaseClicked(currentSelectedItem);
|
|
return;
|
|
}
|
|
|
|
if (IsSelectableEquipmentRewardItem(currentSelectedItem))
|
|
{
|
|
HandleSelectableEquipmentPurchaseClicked(currentSelectedItem);
|
|
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);
|
|
gItemGet.display(gItemGet.FromStoreItem(currentSelectedItem, grantedCount));
|
|
RefreshCurrentView();
|
|
}
|
|
|
|
private void HandlePlayerSkillStorePurchaseClicked(storeItemSO itemSO)
|
|
{
|
|
if (itemSO == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string failureMessage;
|
|
gItemGet.ItemEntry rewardEntry;
|
|
if (!PlayerSkillService.TryPurchasePlayerSkillStoreItem(itemSO, currentPurchaseAmount, out failureMessage, out rewardEntry))
|
|
{
|
|
if (!string.IsNullOrEmpty(failureMessage))
|
|
{
|
|
gNotice.warning.display(failureMessage);
|
|
}
|
|
|
|
RefreshPurchaseSummary();
|
|
return;
|
|
}
|
|
|
|
RegisterPurchasedCount(itemSO.itemID, currentPurchaseAmount);
|
|
gItemGet.display(rewardEntry);
|
|
RefreshCurrentView();
|
|
}
|
|
|
|
private void HandleSelectableEquipmentPurchaseClicked(storeItemSO itemSO)
|
|
{
|
|
if (itemSO == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (currentPurchaseAmount != 1)
|
|
{
|
|
SetPurchaseAmount(1, false);
|
|
gNotice.warning.display("该物品仅支持单份购买");
|
|
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);
|
|
RefreshPurchaseSummary();
|
|
return;
|
|
}
|
|
|
|
bool requireTypeSelection = RequiresTypeSelection(requirement);
|
|
bool requireSkillSelection = RequiresSkillSelection(requirement);
|
|
|
|
if (!requireTypeSelection && !requireSkillSelection)
|
|
{
|
|
TryCompleteSelectableEquipmentPurchase(itemSO, rewardSource, requirement, null, 0);
|
|
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);
|
|
},
|
|
() => { activeCtasInstance = null; });
|
|
}
|
|
|
|
private void TryCompleteSelectableEquipmentPurchase(
|
|
storeItemSO itemSO,
|
|
smeltStageRewardSO rewardSource,
|
|
smeltStageRewardSO.SmeltStageRewardRequirement requirement,
|
|
equipmentSO.EquipmentSkillType? selectedType,
|
|
int selectedSkillGroupId)
|
|
{
|
|
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);
|
|
RefreshPurchaseSummary();
|
|
return;
|
|
}
|
|
|
|
if (!TrySpendCurrency(primaryCost.currencyType, totalCost))
|
|
{
|
|
gNotice.warning.display(insufficientMessage);
|
|
RefreshPurchaseSummary();
|
|
return;
|
|
}
|
|
|
|
equipmentSO generated = equipmentGenerator.GenerateFromSmeltReward(
|
|
rewardSource,
|
|
requirement,
|
|
selectedType,
|
|
selectedSkillGroupId);
|
|
|
|
if (generated == null)
|
|
{
|
|
RefundCurrency(primaryCost.currencyType, totalCost);
|
|
gNotice.warning.display("装备发放失败");
|
|
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 IsCurrentlyPurchasable(itemSO)
|
|
&& (!IsSelectableEquipmentRewardItem(itemSO) || currentPurchaseAmount == 1);
|
|
}
|
|
|
|
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 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();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|