using System; using System.Collections.Generic; using Bansonic; using UnityEngine; using UnityEngine.SceneManagement; [Serializable] public class PlayerSkillSaveData { public int selectedSkillIndex = -1; public int postMatchRewardCounter = 0; public int skillSwitchCooldownRemainingMatches = 0; } public sealed class PlayerSkillService : MonoBehaviour { private const string SaveCategory = "player_skills"; private const string SaveKey = "runtime"; private const string SkillAssetEditorPath = "Assets/playerInfoDisplay/uls/ulsSO.asset"; private const int MemoryBuySkillIndex = 1; private const int MemorySellSkillIndex = 2; private const int PostMatchEquipmentSkillIndex = 4; private const int SmeltDiscountSkillIndex = 5; private const int ScoreToMaterialSkillIndex = 6; private const int RevealLockedIdolSkillsSkillIndex = 7; private const int SpendRefundSkillIndex = 8; private const int ScoreToCoinsSkillIndex = 9; private const int CameraAngleSkillIndex = 10; private const int MemoryBuyStoreItemId = 799001; private const int MemorySellStoreItemId = 799002; private const int RandomMemoryStoreItemId = 78141; private const float SpendRefundPerOwnedHero = 0.005f; private const float MaxSpendRefundRate = 0.05f; public static PlayerSkillService Instance { get; private set; } // Monotonic counter bumped whenever the selected skill (and thus any skill-derived // scene state) may have changed. Per-frame consumers can compare against a cached copy // to skip re-evaluation while the selection is stable. public static int SelectionVersion { get; private set; } private bool initialized; private PlayerSkillSaveData saveData; private userLevel_skills_SO registeredSkillAsset; private storeItemSO cachedRandomMemoryRewardItem; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Bootstrap() { EnsureInstance(); } public static PlayerSkillService EnsureInstance() { if (Instance != null) { return Instance; } GameObject host = new GameObject("__runtime_player_skill_bridge"); host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector; DontDestroyOnLoad(host); Instance = host.AddComponent(); return Instance; } public static void RegisterSkillAsset(userLevel_skills_SO skillAsset) { EnsureInstance().RegisterAssetInternal(skillAsset); } public static bool IsSkillEnabled(int skillIndex) { return EnsureInstance().IsSkillEnabledInternal(skillIndex); } public static int GetSelectedSkillIndex() { return EnsureInstance().GetSelectedSkillIndexInternal(); } public static bool IsRevealLockedSkillsEnabled() { return EnsureInstance().IsSkillEnabledInternal(RevealLockedIdolSkillsSkillIndex); } public static int GetSkillSwitchCooldownRemainingMatches() { return EnsureInstance().GetSkillSwitchCooldownRemainingMatchesInternal(); } public static void ConfirmSkillSelection(userLevel_skills_SO skillAsset, int selectedIndex) { EnsureInstance().ConfirmSkillSelectionInternal(skillAsset, selectedIndex); } public static int ModifySmeltCoinCost(int baseCost) { if (!IsSkillEnabled(SmeltDiscountSkillIndex)) { return Mathf.Max(0, baseCost); } return Mathf.CeilToInt(Mathf.Max(0, baseCost) * 0.5f); } public static int GetCoinRefundAmount(int spentCoins) { return EnsureInstance().GetRefundAmount(spentCoins); } public static int GetMaterialRefundAmount(int spentMaterial) { return EnsureInstance().GetRefundAmount(spentMaterial); } public static void NotifySettlementCompleted(int idolScore) { if (GameConfig.autoPlayEnabled) { return; } EnsureInstance().HandleSettlementCompletedInternal(idolScore); } public static void AppendDynamicStoreItems( List target, Sprite coinSprite, Sprite materialSprite, Sprite memoryBuyPlanSprite, Sprite memorySellPlanSprite) { EnsureInstance().AppendDynamicStoreItemsInternal(target, coinSprite, materialSprite, memoryBuyPlanSprite, memorySellPlanSprite); } public static bool IsPlayerSkillStoreItem(storeItemSO item) { return item != null && (item.itemID == MemoryBuyStoreItemId || item.itemID == MemorySellStoreItemId); } public static bool TryPurchasePlayerSkillStoreItem(storeItemSO item, int amount, out string failureMessage, out gItemGet.ItemEntry rewardEntry) { return EnsureInstance().TryPurchasePlayerSkillStoreItemInternal(item, amount, out failureMessage, out rewardEntry); } public static void ClearPersistentState() { EnsureInstance().ClearPersistentStateInternal(); } public static void ResetToDefaultLevelOneSkill() { EnsureInstance().ResetToDefaultLevelOneSkillInternal(); } private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); InitializeIfNeeded(); } private void OnEnable() { SceneManager.sceneLoaded += HandleSceneLoaded; } private void OnDisable() { SceneManager.sceneLoaded -= HandleSceneLoaded; } private void OnApplicationPause(bool pauseStatus) { if (pauseStatus) { SaveNow(); } } private void OnApplicationQuit() { SaveNow(); } private void InitializeIfNeeded() { if (initialized) { return; } initialized = true; if (!SecureSaveVault.TryLoadJson(SaveCategory, SaveKey, out saveData) || saveData == null) { if (!PlayerProgressBackupService.TryRestorePlayerSkill(out saveData) || saveData == null) { saveData = new PlayerSkillSaveData(); } } ResolveSkillAssetIfNeeded(); SyncSelectionFromAsset(); ApplySceneBindings(); } private void RegisterAssetInternal(userLevel_skills_SO skillAsset) { InitializeIfNeeded(); if (skillAsset == null) { return; } registeredSkillAsset = skillAsset; SyncSelectionFromAsset(); ApplySceneBindings(); } private bool IsSkillEnabledInternal(int skillIndex) { InitializeIfNeeded(); ResolveSkillAssetIfNeeded(); SyncSelectionFromAsset(); return saveData != null && saveData.selectedSkillIndex == skillIndex; } private int GetSelectedSkillIndexInternal() { InitializeIfNeeded(); ResolveSkillAssetIfNeeded(); SyncSelectionFromAsset(); return saveData != null ? saveData.selectedSkillIndex : -1; } private int GetSkillSwitchCooldownRemainingMatchesInternal() { InitializeIfNeeded(); return saveData != null ? Mathf.Max(0, saveData.skillSwitchCooldownRemainingMatches) : 0; } private int GetRefundAmount(int spendAmount) { if (spendAmount <= 0 || !IsSkillEnabledInternal(SpendRefundSkillIndex)) { return 0; } float refundRate = Mathf.Min(GetOwnedHeroCount() * SpendRefundPerOwnedHero, MaxSpendRefundRate); if (refundRate <= 0f) { return 0; } return Mathf.FloorToInt(spendAmount * refundRate); } private void HandleSettlementCompletedInternal(int idolScore) { InitializeIfNeeded(); ResolveSkillAssetIfNeeded(); SyncSelectionFromAsset(); if (saveData.skillSwitchCooldownRemainingMatches > 0) { saveData.skillSwitchCooldownRemainingMatches = Mathf.Max(0, saveData.skillSwitchCooldownRemainingMatches - 1); } List grantedEntries = new List(); if (IsSkillEnabledInternal(PostMatchEquipmentSkillIndex)) { saveData.postMatchRewardCounter = Mathf.Max(0, saveData.postMatchRewardCounter) + 1; if (saveData.postMatchRewardCounter >= 10) { saveData.postMatchRewardCounter = 0; equipmentSO generated = GrantRandomMemoryFromConfiguredSource(); if (generated != null) { grantedEntries.Add(gItemGet.FromEquipment(generated, 1)); } } } if (idolScore > 0 && IsSkillEnabledInternal(ScoreToMaterialSkillIndex)) { int grantedMaterial = Mathf.FloorToInt(idolScore * 0.001f); if (grantedMaterial > 0) { PlayerEconomyLedger.EnsureInstance().AddMaterial(grantedMaterial); grantedEntries.Add(gItemGet.Create("\u8bb0\u5fc6\u788e\u7247", null, grantedMaterial, ItemRarity.Uncommon)); } } if (idolScore > 0 && IsSkillEnabledInternal(ScoreToCoinsSkillIndex)) { int grantedCoins = Mathf.FloorToInt(idolScore * 0.002f); if (grantedCoins > 0) { PlayerEconomyLedger.EnsureInstance().AddCoins(grantedCoins); grantedEntries.Add(gItemGet.Create("算力", null, grantedCoins, new Color32(240, 196, 74, 255))); } } SaveNow(); if (grantedEntries.Count > 0) { gItemGet.display(grantedEntries); } } private void AppendDynamicStoreItemsInternal( List target, Sprite coinSprite, Sprite materialSprite, Sprite memoryBuyPlanSprite, Sprite memorySellPlanSprite) { InitializeIfNeeded(); ResolveSkillAssetIfNeeded(); SyncSelectionFromAsset(); if (target == null) { return; } if (IsSkillEnabledInternal(MemoryBuySkillIndex)) { target.Add(CreateMemoryBuyStoreItem(memoryBuyPlanSprite != null ? memoryBuyPlanSprite : materialSprite)); } if (IsSkillEnabledInternal(MemorySellSkillIndex)) { target.Add(CreateMemorySellStoreItem(memorySellPlanSprite != null ? memorySellPlanSprite : coinSprite)); } } private bool TryPurchasePlayerSkillStoreItemInternal(storeItemSO item, int amount, out string failureMessage, out gItemGet.ItemEntry rewardEntry) { failureMessage = string.Empty; rewardEntry = default; if (item == null) { failureMessage = "\u5546\u54c1\u6570\u636e\u4e22\u5931"; return false; } int purchaseAmount = Mathf.Max(1, amount); if (item.itemID == MemoryBuyStoreItemId) { int totalCost = purchaseAmount * 10; if (!PlayerEconomyLedger.EnsureInstance().TrySpendCoins(totalCost)) { failureMessage = "\u8d27\u5e01\u4e0d\u8db3"; return false; } PlayerEconomyLedger.EnsureInstance().AddMaterial(purchaseAmount); rewardEntry = gItemGet.Create("\u8bb0\u5fc6\u788e\u7247", item.itemIcon, purchaseAmount, ItemRarity.Uncommon); return true; } if (item.itemID == MemorySellStoreItemId) { if (!PlayerEconomyLedger.EnsureInstance().TrySpendMaterial(purchaseAmount)) { failureMessage = "\u8bb0\u5fc6\u788e\u7247\u4e0d\u8db3"; return false; } int grantedCoins = purchaseAmount * 4; PlayerEconomyLedger.EnsureInstance().AddCoins(grantedCoins); rewardEntry = gItemGet.Create("算力", item.itemIcon, grantedCoins, new Color32(240, 196, 74, 255)); return true; } failureMessage = "\u5f53\u524d\u672a\u914d\u7f6e\u53d1\u653e\u903b\u8f91"; return false; } private void ConfirmSkillSelectionInternal(userLevel_skills_SO skillAsset, int selectedIndex) { InitializeIfNeeded(); if (skillAsset == null || skillAsset.skills == null || selectedIndex < 0 || selectedIndex >= skillAsset.skills.Count) { return; } registeredSkillAsset = skillAsset; for (int i = 0; i < skillAsset.skills.Count; i++) { userLevel_skills_SO.UserLevelSkillEntry entry = skillAsset.skills[i]; if (entry == null) { continue; } entry.isEnabled = (i == selectedIndex); } if (saveData.selectedSkillIndex != selectedIndex) { saveData.postMatchRewardCounter = 0; saveData.skillSwitchCooldownRemainingMatches = 3; } saveData.selectedSkillIndex = selectedIndex; #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(skillAsset); } #endif SaveNow(); ApplySceneBindings(); } private void ClearPersistentStateInternal() { saveData = new PlayerSkillSaveData(); SecureSaveVault.Delete(SaveCategory, SaveKey); PlayerProgressBackupService.ClearPlayerSkillBackup(); ApplySceneBindings(); } private void ResetToDefaultLevelOneSkillInternal() { InitializeIfNeeded(); ResolveSkillAssetIfNeeded(); saveData = saveData ?? new PlayerSkillSaveData(); saveData.postMatchRewardCounter = 0; saveData.skillSwitchCooldownRemainingMatches = 0; int defaultIndex = -1; if (registeredSkillAsset != null && registeredSkillAsset.skills != null && registeredSkillAsset.skills.Count > 0) { defaultIndex = 0; for (int i = 0; i < registeredSkillAsset.skills.Count; i++) { userLevel_skills_SO.UserLevelSkillEntry entry = registeredSkillAsset.skills[i]; if (entry != null) { entry.isEnabled = i == defaultIndex; } } } saveData.selectedSkillIndex = defaultIndex; SaveNow(); ApplySceneBindings(); } private void HandleSceneLoaded(Scene scene, LoadSceneMode mode) { ApplySceneBindings(); } private void ApplySceneBindings() { // Selection-derived scene state is being (re)applied here; advance the version so // per-frame consumers know to re-read it once. SelectionVersion++; bool revealLockedSkills = IsSkillEnabledInternal(RevealLockedIdolSkillsSkillIndex); idolSkillsHub[] hubs = FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); for (int i = 0; i < hubs.Length; i++) { idolSkillsHub hub = hubs[i]; if (hub == null) { continue; } if (hub.display_ungotten != revealLockedSkills) { hub.display_ungotten = revealLockedSkills; if (hub.gameObject.activeInHierarchy) { hub.Rebuild(); } } } storeSystem[] stores = FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); for (int i = 0; i < stores.Length; i++) { if (stores[i] != null) { stores[i].RefreshPlayerSkillItems(); } } equipSmelt[] smelters = FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); for (int i = 0; i < smelters.Length; i++) { if (smelters[i] != null) { smelters[i].RefreshPlayerSkillAdjustedUi(); } } effectEventController.RefreshPlayerSkillCameraOffset(); } private void ResolveSkillAssetIfNeeded() { if (registeredSkillAsset != null) { return; } uLevel_skills[] skillPanels = FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); for (int i = 0; i < skillPanels.Length; i++) { if (skillPanels[i] != null && skillPanels[i].ulsSO != null) { registeredSkillAsset = skillPanels[i].ulsSO; return; } } userLevel_skills_SO[] loadedAssets = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < loadedAssets.Length; i++) { if (loadedAssets[i] != null) { registeredSkillAsset = loadedAssets[i]; return; } } #if UNITY_EDITOR registeredSkillAsset = UnityEditor.AssetDatabase.LoadAssetAtPath(SkillAssetEditorPath); #endif } private void SyncSelectionFromAsset() { if (saveData == null || registeredSkillAsset == null || registeredSkillAsset.skills == null || registeredSkillAsset.skills.Count == 0) { return; } bool assetChanged = false; if (saveData.selectedSkillIndex >= 0 && saveData.selectedSkillIndex < registeredSkillAsset.skills.Count) { for (int i = 0; i < registeredSkillAsset.skills.Count; i++) { userLevel_skills_SO.UserLevelSkillEntry entry = registeredSkillAsset.skills[i]; if (entry == null) { continue; } bool shouldEnable = i == saveData.selectedSkillIndex; if (entry.isEnabled != shouldEnable) { entry.isEnabled = shouldEnable; assetChanged = true; } } #if UNITY_EDITOR if (assetChanged && !Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(registeredSkillAsset); } #endif return; } int enabledIndex = -1; for (int i = 0; i < registeredSkillAsset.skills.Count; i++) { userLevel_skills_SO.UserLevelSkillEntry entry = registeredSkillAsset.skills[i]; if (entry != null && entry.isEnabled) { enabledIndex = i; break; } } if (enabledIndex == saveData.selectedSkillIndex) { return; } if (saveData.selectedSkillIndex >= 0) { saveData.postMatchRewardCounter = 0; } saveData.selectedSkillIndex = enabledIndex; // Selection was repaired from the asset's enabled flags without going through // ApplySceneBindings; bump the version so per-frame consumers re-read it. SelectionVersion++; SaveNow(); } private storeItemSO CreateMemoryBuyStoreItem(Sprite materialSprite) { storeItemSO item = ScriptableObject.CreateInstance(); item.hideFlags = HideFlags.DontSave; item.itemID = MemoryBuyStoreItemId; item.itemName = "1\u8bb0\u5fc6\u788e\u7247"; item.itemIcon = materialSprite; item.itemRarity = ItemRarity.Uncommon; item.itemSinglePurchaseQty = 1; item.itemPurchaseQuota = -1; item.itemDescription = "\u82b1\u8d3910\u91d1\u5e01\u8d2d\u4e701\u4e2a\u8bb0\u5fc6\u788e\u7247\u3002"; item.itemDetailedDescription = "\u73a9\u5bb6\u6280\u80fd\u201c\u8bb0\u5fc6\u8d2d\u4e70\u8ba1\u5212\u201d\u751f\u6548\u4e2d\u3002\u82b1\u8d3910\u91d1\u5e01\u8d2d\u4e701\u4e2a\u8bb0\u5fc6\u788e\u7247\u3002"; item.isOnShelf = true; item.canbepurchased = true; item.itemType = storeItemSO.ItemType.consumable; item.costRequirements = new List { new storeItemSO.CostRequirement { currencyType = storeItemSO.CurrencyType.coins, amount = 10 } }; item.unlockRequirements = new List(); return item; } private storeItemSO CreateMemorySellStoreItem(Sprite coinSprite) { storeItemSO item = ScriptableObject.CreateInstance(); item.hideFlags = HideFlags.DontSave; item.itemID = MemorySellStoreItemId; item.itemName = "4\u91d1\u5e01"; item.itemIcon = coinSprite; item.itemRarity = ItemRarity.Common; item.itemSinglePurchaseQty = 1; item.itemPurchaseQuota = -1; item.itemDescription = "\u6d88\u80171\u4e2a\u8bb0\u5fc6\u788e\u7247\uff0c\u6362\u53d64\u91d1\u5e01\u3002"; item.itemDetailedDescription = "\u73a9\u5bb6\u6280\u80fd\u201c\u8bb0\u5fc6\u5f52\u8fd8\u8ba1\u5212\u201d\u751f\u6548\u4e2d\u3002\u6d88\u80171\u4e2a\u8bb0\u5fc6\u788e\u7247\uff0c\u6362\u53d64\u91d1\u5e01\u3002"; item.isOnShelf = true; item.canbepurchased = true; item.itemType = storeItemSO.ItemType.consumable; item.costRequirements = new List { new storeItemSO.CostRequirement { currencyType = storeItemSO.CurrencyType.material, amount = 1 } }; item.unlockRequirements = new List(); return item; } private equipmentSO GrantRandomMemoryFromConfiguredSource() { storeItemSO randomMemoryItem = ResolveRandomMemoryRewardItem(); if (randomMemoryItem == null || randomMemoryItem.associatedSelectableEquipmentRewardSource == null) { return null; } smeltStageRewardSO rewardSource = randomMemoryItem.associatedSelectableEquipmentRewardSource; int rewardIndex = Mathf.Max(0, randomMemoryItem.associatedSelectableEquipmentRewardIndex); if (rewardSource.rewardRequirements == null || rewardIndex >= rewardSource.rewardRequirements.Length) { return null; } return equipmentGenerator.GenerateFromSmeltReward(rewardSource, rewardSource.rewardRequirements[rewardIndex]); } private storeItemSO ResolveRandomMemoryRewardItem() { if (cachedRandomMemoryRewardItem != null) { return cachedRandomMemoryRewardItem; } storeItemSO[] items = RuntimeResourcesCache.LoadAllStoreItems(); for (int i = 0; i < items.Length; i++) { if (items[i] != null && items[i].itemID == RandomMemoryStoreItemId) { cachedRandomMemoryRewardItem = items[i]; break; } } return cachedRandomMemoryRewardItem; } private int GetOwnedHeroCount() { HashSet heroIds = new HashSet(); AllyHero_SO[] heroes = RuntimeResourcesCache.LoadAllAllyHeroes(); if (heroes == null) { return 0; } for (int i = 0; i < heroes.Length; i++) { AllyHero_SO hero = heroes[i]; if (hero != null && hero.isUnlocked) { heroIds.Add(hero.ally_heroID); } } return heroIds.Count; } private bool IsSceneInstance(Component component) { if (component == null || component.gameObject == null) { return false; } Scene scene = component.gameObject.scene; return scene.IsValid() && scene.isLoaded; } private void SaveNow() { if (!initialized) { return; } SecureSaveVault.SaveJson(SaveCategory, SaveKey, saveData); PlayerProgressBackupService.SavePlayerSkill(saveData); } }